Highly customizable APIs need to somehow expose a long list of parameters that may be set, and will want to add new parameters in a backwards compatible way. Optional arguments allow that to be expressed in a single function.
From the caller side, you may ergonomically provide any subset of the optional arguments and are able to express your own uncertainty about the value. Reasoning about call sites remains simple as it's easy to tell if you're dealing with optional arguments or not.
In the ecosystem, many uses of the builder pattern can be identified as a need for this feature.
This RFC proposes that optional arguments be named. It does not propose named arguments as a general feature, leaving that as an extension. The reason to propose optional arguments first and named arguments as an extension is that optional arguments add less reasoning complexity than named aguments. As a wise man once put it:
Named arguments introduce ambiguity in how to write and how to read all call sites everywhere, and makes the grammar and reasoning about arguments strictly more complex, across the language and all libraries.
A feature that is not complex however:
Is ignorable by most users and only even appears necessary when you need them.
Which is what we propose optional arguments to be, a feature that should be used only by libraries that really need them. The reasoning about existing call sites does not change because we make the presence of optional arguments syntatically obvious. If we should design the less complex feature first, then optional arguments should be designed first.
A new syntatical form is introduced for functions with optional arguments or optargs for short.
Signature and implementation look like:
fn func(need: i32, opt maybe: i32, opt bar: Bar) {
let maybe = bar.unwrap_or(4); // `maybe` has a simple default.
// Complex logic for dealing with the presence or absence of `bar`.
if let Some(bar) = bar {
// ...
} else { /* ... */ }
// ...
}
Same as usual functions but with the opt
appearing behind optargs, binding the value as an option.
Here need
is a normal argument but maybe
and bar
are optargs.
Some examples of how to call func
:
func(0, maybe: 0, bar: Bar::new(), ..) // Nothing ommited.
func(1, bar: Bar::new(), maybe: 0, ..) // Nothing ommited, order swapped.
func(2, maybe: 2, ..) // `bar` ommited.
func(3, bar: Bar::new(), ..) // `maybe` ommited.
func(4, ..) // Both ommited.
// Optargs may be provided conditionally by using pattern matching.
func(5, Ok(maybe): "5".parse(), Some(bar): check_bar(), ..)
The non-positional requirement brings us to introduce named arguments in call sites. An optarg may be ommited from the call site, which results in it being bound as None
. The dots are mandatory and represent ommiting any other optional arguments, including those that don't yet exist.
The name may be contained in a pattern as in Some(name): value
. If the pattern is matched then the value bound by name
is provided as the optarg, otherwise the optarg is ommited. Some(name): Some(value)
is therefore equivalent to name: value
and Some(name): None
is equivalent ommiting the optarg.
There are many different possible designs for this feature, as can be seen in previous discussions and in other languages. Here we stabilish the requirements that will guide this design. The general principle is that we should first optimize the common case and then attempt to retain generality. Our target use case is APIs that expose a list of configurable parameters.
Functions that don't have optargs are the only existing case and will continue to be the common case. The fact that a normal function call exhausts it's arguments simplifies reasoning for the user. To avoid pessimizing the common case, we should not add reasoning complexity to reading normal functions.
APIs that have a long list of configurable parameters are likely to add more in the future, therefore optargs should allow adding arguments in a backwards compatible way. But allowing optargs to be backwards compatibly added to any function would conflict with requirement 1, therefore adding an optarg to a function that has none is a breaking change, however adding another optarg is not a breaking change.
When there only few possibilities of arguments, making multiple functions with descriptive names as is done today is the accepted style. Optargs should be left to the use cases with a large number of optional arguments where the creation of multiple functions is impractical, therefore we must support maximum flexibility in the choice of optargs to supply.
There are many different semantics the API author may express with optargs.
The optarg may be given a default value, may be left as an Option
or may require complex logic to deal with it's presence or omission. The precise semantics that can be relied upon should be part of the documentation of each function. For example if there is a default value it is up to the API author to decide if it should be documented.
Sometimes the callers are themselves dealing with uncertainty on the presence of a value, for example in the form of an Option
or Result
. Requiring something like:
if let Some(bar) = bar {
func(bar: bar, ..)
else {
func(..)
}
Is an unnaceptable burden on the caller. Instead we allow uncertainty to be ergonomically expressed through patterns. When binding a value to a name Rust always allows you to use a pattern, and here is no different.
Having this requirement is great for lowering the cognitive load, as dicussed in the motivation section. However future extensions may forgo it in favor of sweeter syntax or more general use of named parameters.
An argument is an optarg iff it is has opt
(new contextual keyword) before the pattern. In:
fn func(mut opt x : Foo, opt ref y : Bar)
x
and y
are optargs. There must be exactly one variable bound in a pattern prefixed by opt
, for example opt (a, b): (i32, i32)
is invalid. The identifier of that variable is the name of the optarg.
Optargs must be named, which is a new syntatical form where the argument must be prefixed by it's name in call sites. Non-optional arguments must not be named. The grammar of named arguments is pattern: expr
where pattern
must bind exactly one identifier whose name matches the name of an optarg. If the pattern is refuted, the argument is treated as omitted. For example func(Some(x): val, ..)
where val
is of type Option<Foo>
binds x
if val is Some
.
If an optarg is omitted from the call site, the respective pattern will be bound as None
. In the example above, if ommited and y
would be bound as None
with type Option<&Bar>
.
On call sites and signatures, optargs must come after non-optional arguments. The relative order of optargs in call sites does not have to match the order in the signature.
On call sites, a function that has optargs must have it's argument list suffixed with ..
.
This fullfills requirement 1 even if no optargs are provided, for example in func(bar, ..)
or simply func(..)
.
Being an optarg is part of the public API and makes the name also part of the public API. An optarg will be prefixed by opt
in the docs.
Closures can also have optargs.
For optargs the name is part of the type and is prefixed with opt
. For example Fn(Foo, opt name: Bar)
-
Discussing named args by themselves as a stepping stone. This RFC is explicitly motivated towards optargs, which will always be part of the motivation for named args. Discussing named args in isolation is unlikely to lead to a good design for optargs.
-
Using
:
as a name-value separator conflicts with type ascription. Some other candidates are:
,->
,<-
,=>
-
Default args, essentially confict with requirement 4 due to favoring "simple default" semantics.
- Allow binding multiple optargs per pattern in the call site, as in
func((name1, name2): tuple)
The following future extensions break soft requirement 6:
- Allowing normal parameters to be named.
- Sugar for ommiting optional parameters names as in FRU, you may pass just
func(name, ..)
instead offunc(name: name, ..)
.
Is func(..)
a grammatical ambiguity?
What are the consequences for ABI and FFI?