How can I have global configuration parameters for a module/package?

I’m not aware if there is already a place where this is documented, but I found GitHub - typst-community/guidelines: A guideline document for Typst package and template development., which might help get you started. You still have to consider the scale of the package and how would it scale in the future. For very simple packages, you probably should use the simplest solution, but for more involved ones, something else might be a better choice. The kind of package can also change what would be the best approach.

I will give you my thought on this. Generally speaking, I only know of 2 main approaches — carrying the state within function arguments or using state(). Indeed, using state can become bulky and a little less unnatural, but some packages use context right in the main function, so if a user doesn’t need to read the output of your functions, then I think it is safe to use context when calling them. Passing the same state/options through many functions can also feel tedious, but dealing with pure functions is something a lot of people don’t really face on a daily basis (unless they use Haskell).

With the context approach, you can modify your new options at any point and then get the correct version with options.get(). I suspect that Codly uses this, but haven’t checked. I think Touying also uses something similar with metadata().

I have rubby (which is due for a rewrite for a long time), where you call an initialization function that returns the function with all settings already baked in. So your 3rd point is very similar:

// colorful.typ
#let paragraph(color) = body => text(color, body)
#let note(color) = body => footnote(text(color, body))

#let init(color: blue) = (
  paragraph: paragraph(color),
  note: note(color),
)

#import "colorful.typ"

#let colorful = colorful.init(color: yellow)

#(colorful.paragraph)[
  This is a colorful paragraph
  #(colorful.note)[And a colorful footnote.]
]

But as you can see, the lack of custom user types makes it not ideal to use it like this. This can be changed slightly:

#import "colorful.typ"

#let colorful = colorful.init(color: yellow)

#let paragraph = colorful.paragraph
#let note = colorful.note

#paragraph[This is a colorful paragraph #note[And a colorful footnote.]]

You can also just ditch the init function and configure the functions directly:

#import "colorful.typ"

#let paragraph = colorful.paragraph(yellow)
#let note = colorful.note(green)

#paragraph[This is a colorful paragraph #note[And a colorful footnote.]]

Or use a different approach:

// colorful.typ
#let DEFAULT = (color: blue)

#let paragraph(color: DEFAULT.color, body) = text(color, body)
#let note(color: DEFAULT.color, body) = footnote(text(color, body))

#import "colorful.typ": paragraph, note

#let paragraph = paragraph.with(color: yellow)
#let note = note.with(color: green)

#paragraph[This is a colorful paragraph #note[And a colorful footnote.]]

You can see that both functions now can be customized directly, but you can just use them as-is to get the default behavior. One downside of having this kind of centralized DEFAULT is that LSP probably won’t show that the default color is blue, but rather DEFAULT.color. This is something you probably should keep in mind.

Here is a state approach:

// colorful.typ
#let options = state("_colorful package options", (:))

#let set-options(color: blue) = options.update((color: color))
#let get(option) = {
  assert(option in options.get(), message: "First run set-options().")
  options.get().at(option)
}

#let paragraph(body) = context text(get("color"), body)
#let note(body) = context footnote(text(get("color"), body))

#import "colorful.typ": paragraph, note, set-options

#set-options(color: green)

#paragraph[This is a colorful paragraph #note[And a colorful footnote.]]

Here, since the source of truth is only the set-options function, that is why you have to call it at least once. I’m not user if there is another way to do this. But if you introduce duplication of the default values, then you’re no longer required to call that in order to use your functions. Another small problem is that you would specify the options with string, which is not a great DX.

// colorful.typ
#let options = state("_colorful package options", (color: blue))
#let set-options(color: blue) = options.update((color: color))

#let paragraph(body) = context text(options.get().color, body)
#let note(body) = context footnote(text(options.get().color, body))

Now you have option autocompletion, and the user can use those functions right away, but the color is defined twice, though the definitions are close to each other, so this might not be too bad.

Again, I think it really depends on your exact package: what API it should have, what functionality it provides, how and where should this be used by users. And there are many more variations of what I laid out, so you might find something that suits you better. You can also find some packages and see how they are structured from the inside, see what people are doing.

5 Likes