The ease of scripting in Typst is absolutely fascinating but I find myself having difficulties building a mental model of positional vs. named arguments, especially in comparison to how these concepts are handled in languages like R and Python. Could you help me discuss whether the following observations are correct?
My mental model based on reading the Typst function documentation, as well as trial and error:
There are positional arguments and named arguments, as well as argument sinks.
Whether an argument is positional or named depends on how the function is defined, not how it is called – unless an argument sink is used (see myfun below).
An argument is called “named” if it has a default value and “positional” otherwise (ignoring argument sinks here).
All arguments without default values (“positional arguments”) are mandatory.
An argument with a default value (“named argument”) can only be specified in a function call by referring to its name (i.e., it will not be matched by position). This is true for #alert() as defined below but not for functions with special functionality as discussed here, e.g., #text():
#text(green)[blah]
#text(fill: green)[blah]
#let alert(content, colored: true) = {
set text(red) if colored
content
}
#alert[blah]
#alert(colored: false)[blah]
// does not work:
// #alert(false)[blah]
If the above observations are true, I personally would have an easier time understanding the different types of arguments in Typst with the following terminology:
When defining a function, arguments may have default values.
When calling a function, arguments with default values can only be specified by referring to their name; arguments without default values can only be specified by position (i.e., arguments can be named or unnamed).
I think your statements are all correct, but I’m not sure your proposed wording is better. First note that this part is misleading:
It’s true that the function definition determines what is positional and what is named. But the whole idea is how you are going to call the function, how Typst will know which value is for which parameter… Is it just about first value, second value, etc. ? Or does the caller specify names?
Note the correct terminology: the names written in the function definition are the function parameters. The values passed when the function is called are arguments. When you call a function, if you give a name to a value then it’s a named argument. Otherwise it’s positional.
Consider this:
#let f(a, x: none) = ...
All the parameters are named in the definitions, but the point is that when you call f(3, x: [text]), you don’t give a name for 3 but you do for [text].
The fact that arguments are named if and only if they have a default value is a bit incidental, I guess it might change in a future version…
As for the linked “special functionality” of text: it’s just not well documented how text() accepts some values either as positional or as named argument. But as described in the linked discussion, you can implement the same functionality in your own functions using argument sinks: with a sink the function can accept any number of positional and named arguments, and do whatever it wants with these.
Thanks for your warm welcome in the forum and taking the time to share your thoughts! I did not necessarily mean to propose a new wording, I am mainly trying to wrap my head around how things work in Typst. For example, I was not aware that Typst strictly refers to “parameters” when talking about the function definition.
I’m not so sure about your statement that “the fact that arguments are named if and only if they have a default value is a bit incidental” – re-reading the Typst documentation there seems to be a clear link:
The parameter list can contain mandatory positional parameters, named parameters with default values and argument sinks.
Otherwise, how else could you specify what is positional or named in the function definition?
The other thing I was struggling with is how a function is called. If whether a parameter is positional or named is determined in the function definition and not by the way a function is called, I was expecting that named arguments would be matched to the corresponding function parameter and that any unnamed arguments would be matched by position (as is the case in R and other programming languages). However, this is not true in Typst:
#let f(a, x: [Typst]) = [#a #x]
// works
#f([Hi], x: [there])
// does not work
// #f([Hi], [there])
// does not work
// #f(a: [Hi], x: [there])
According to my experience, positional parameters only accept unnamed arguments, while named parameters only accept named arguments. And the order of positional arguments are filling the positional parameters one by one. And all the positional parameters need to be filled; otherwise, errors occur. The order of named arguments are not very important.
Sometimes it is annoying. Like the datetime() function. Everytime when I want to create a datetime object, I have to write a long line giving all the parameters’ names.
This is less Typst specific and more general programming language jargon. Another variation I’ve heard is formal and actual parameters instead of parameters/arguments, so I’d say there is also not complete consensus within the programming community on these words. You are definitely not expected to know this as a Typst user, but in the context of this discussion it’s helpful to work with vocabulary that has already been established.
That’s true, however the docs here reflect how Typst is and don’t necessarily imply that Typst should be this way. There have been discussions about “named parameter = parameter with default value” being unnecessarily limiting, and Typst’s developers agree, so once a good syntax for lifting this limitation is found, the two may stop being linked.
Personally, I use the following mental model:
It is clear from the call site which arguments are positional and which are named: anything of the form foo: ... is named and corresponds to name foo, anything else is positional and its index is the one in the argument list, ignoring named arguments while counting. (EDIT: I forgot to account for argument spreading: arrays, dictionaries and arguments objects also contribute to the positional, named, and both kinds of arguments.)
Likewise, it is clear from a function definition which parameters are positional or named, and whether there’s an argument sink.
When applying the arguments to the function being called, consider the declared parameters:
All positional arguments are assigned to corresponding positional parameters. If any parameters didn’t receive a value (i.e. there are too few positional arguments), the call fails.
All named arguments are assigned to corresponding named parameters. If any parameters didn’t receive a value (i.e. there is no named arguments with its name), the parameter is assigned its default value.
If any arguments are left over and there is an argument sink, they are put into the argument sink. Otherwise, the call fails.
Although a bit wordy, this should describe precisely how user defined functions behave – there are exceptions among built-in functions such as text().
I think it’s better to treat these separately than trying to make a rule that encompasses them, since they are literally exceptions that are only possible because they are built into Typst. A user could not do that, except by going through an argument sink first, which is covered by the rules.
Thanks for you two chiming in as well! I think I have a good enough understanding now to enjoy Typst in its current form and I’m eager to see what future developments will bring. Forgive me for being ignorant about the ongoing discussions in Typst’s developer community!
Your observations are mostly correct! In Typst, whether an argument is positional or named depends on the function definition, not how it’s called (unless using an argument sink). Named arguments require explicit names if they have default values, while positional arguments are mandatory. The distinction is similar to Python but differs from R, where positional arguments can sometimes override named ones.