<import "base.kuht" as "base" />
<head>
<title>Named and Optional Arguments are Awesome</title>
<meta name="description" content="Whenever someone asks what my least favorite part of Rust is, my answer is always the same: it doesn't have named or optional arguments. This article explores the ideas for adding them, both good and bad." />
</head>
<body>
<article>
<h1>Named and Optional Arguments are Awesome</h1>
<p>
Whenever someone asks what my least favorite part of Rust is, my answer is
always the same: it doesn't have named or optional arguments.
</p>
<p>
I have a small list of programming languages I tolerate: Rust, C#, TypeScript,
Dart, Python. Of those languages, Rust is the hardest to get named arguments
out of. I'm going to start this post by going over the other languages and how
they've achieved named arguments. Then, we'll look at Rust, the problems that
have resulted from not having named arguments, and proposals for how Rust
could get them in the future.
</p>
<h2>How Other Languages Handle Named Parameters</h2>
<h3>Dart</h3>
<p>
Dart is designed specifically for graphical user interfaces. GUI components
tend to have lots of optional parameters, so Dart tried very hard to get them
right. And I think it does the best job out of any language we're going to
talk about today.
</p>
<p>
First, let's look at a normal function, just to familiarize ourselves with the
language:
</p>
<pre>
Widget Text(String text) {
// ...
}
</pre>
<p>
That's pretty normal as far as languages go. You can probably guess what it
does, even if you don't know Dart. Dart was basically designed in a lab to be
easy for programmers to learn.
</p>
<p>
Now, let's add a named parameter to set the size of the text, and default it to
16 pixels:
</p>
<pre>
Widget Text(String text, {int fontSize = 16}) { /* ... */ }
// usage:
Text("Hello, world!", fontSize: 24)
</pre>
<p>
Named parameters are wrapped in curly braces. This makes them look similar to
a map. Named parameters can also be defaulted to null, or even required.
</p>
<pre>
Widget Text({
int fontSize = 16,
Color? color,
required String text,
});
</pre>
<p>
Dart also supports optional positional arguments by wrapping the parameter in
square brackets.
</p>
<pre>
// From the Dart documentation
String say(String from, String msg, [String device = 'carrier pigeon']) {
var result = '$from says $msg with a $device';
return result;
}
assert(say('Bob', 'Howdy') == 'Bob says Howdy with a carrier pigeon');
assert(say('Bob', 'Howdy', 'smoke signal') == 'Bob says Howdy with a smoke signal');
</pre>
<h3>C#</h3>
<p>
C#'s handling of named parameters is also pretty good. In fact, any parameter can be
named.
</p>
<pre>
void ExampleMethod(string foo, string bar)
{
Console.WriteLine($"{foo} {bar}")
}
ExampleMethod(bar: "Hello", foo: "world");
</pre>
<p>
And any parameter can have a default value, as long as they're specified after
the required parameters.
</p>
<pre>
void ExampleMethod(string foo, string bar = "world")
{
Console.WriteLine($"{foo} {bar}")
}
ExampleMethod("Howdy");
</pre>
And that's all there is to it. I like the simplicity of it.
<h3>TypeScript</h3>
<p>
TypeScript's handling of named parameters is not very good, in my opinion.
But optional positional parameters aren't named, and they work pretty well, so
let's start with that.
</p>
<pre>
function example(required: string, optional?: string = "world"): string {
return `${required} ${optional}`;
}
example("Hello") === "Hello world";
example("Hello", "TypeScript") === "Hello TypeScript";
</pre>
<p>
That seems fine to me. If you don't specify the default value, then it gets
set to <code>undefined</code>. The problem is that there isn't a good built-in
syntax for named parameters, so we have to use other TypeScript features to
hack it in.
</p>
<pre>
function example(
required: string,
{
requiredNamed,
namedOptional = "world"
}: { requiredNamed: string, namedOptional?: string }
): string {
return `${required} ${requiredNamed} ${namedOptional}`;
}
example("Hello", { requiredNamed: "beautiful" });
</pre>
<p>
Here, we're taking advantage of both anonymous types and destructuring. The
second parameter of the function has an anonymous object type, which contains
two fields: <code>requiredNamed</code> and <code>namedOptional</code>. Then
we destructure this parameter so we can use the fields in the function.
</p>
<p>
The most annoying part of this is that we have to define the field names
twice. Once for the type, and once for the function parameters. This is
unnecessary verbosity that any good language should try to avoid. The function
call is also more verbose than necessary, because of the curly braces.
</p>
<p>
Unfortunately, this syntax is used all of the time in React. Components are
typically a function that takes one argument, which is an object. So when you
define a component, you have to do this constantly.
</p>
<h3>Rust</h3>
<p>
Some people have tried to do named parameters in Rust. This generally doesn't
work very well. The most common approach is to define a struct for the
function, and then have it implement the <code>Default</code> trait, so that
we can end the struct initializer with <code>..Default::default()</code>.
</p>
<pre>
struct ExampleParams {
foo: String,
bar: String,
}
impl Default for ExampleParams {
fn default() -> Self {
Self {
foo: "Hello".into(),
bar: "Hello".into(),
}
}
}
fn example(Example { foo, bar }) -> String {
format!("{foo} {bar}")
}
assert_eq!(
example(Example { foo: "Hello".into(), ..Default::default() }),
"Hello world".to_string()
);
</pre>
<p>
I complained before about how verbose TypeScript's version is, but this is
even worse, because the types are not anonymous. And worse, if your default
values are not the default of the types (e.g. empty string for <code>String</code>),
then you need to write a whole function to set the default values. It also
only works if every single parameter has a default value. If only some of the
parameters have default values, then you need an even more verbose solution.
</p>
<h2>Why Rust Needs Optional Parameters</h2>
<p>
The standard library doesn't tend to follow the above pattern (for good reason,
in my opinion). But there are still several cases where the standard library
probably would have benefitted from it.
</p>
<p>Take some of the factories of <code>HashMap</code>, for example.</p>
<pre>
impl<K, V> HashMap<K, V, RandomState> {
pub fn new() -> HashMap<K, V, RandomState>;
pub fn with_capacity(capacity: usize) -> HashMap<K, V, RandomState>;
}
impl<K, V, A: Allocator> HashMap<K, V, RandomState, A> {
pub fn new_in(alloc: A) -> Self;
pub fn with_capacity_in(capacity: usize, alloc: A) -> Self;
}
impl<K, V, S> HashMap<K, V, S> {
pub const fn with_hasher(hash_builder: S) -> HashMap<K, V, S>;
pub fn with_capacity_and_hasher(capacity: usize, hasher: S) -> HashMap<K, V, S>;
}
impl<K, V, S, A: Allocator> HashMap<K, V, S, A> {
pub fn with_hasher_in(hash_builder: S, alloc: A) -> Self;
pub fn with_capacity_and_hasher_in(capacity: usize, hasher: S, alloc: A) -> Self;
}
</pre>
<p>
With just three optional parameters, we need eight functions to represent all
of the overloads. Each one of them needs to have their own name, since Rust
doesn't actually have overloads. And the programmer and/or code reviewer must
either memorize or look up the order of the parameters when one of the more
complex factories are used.
</p>
<p>
Another problem that comes up is the fact that, when the parameters are
unnamed, it can be easier to make mistakes. Consider the following print
function.
</p>
<pre>
fn print(text: &str, bold: bool, italics: bool, underline: bool);
</pre>
<p>
It's easy to mix up the parameters here because they don't have names. If I
forget that the <code>italics</code> parameter is third and not second, then I
can end up accidentally bolding my text instead.
</p>
<p>
Some functions just have lots of parameters as well. Imagine if I added color,
underline color, background color, blinking text, hidden text, circled text,
and fast blinking text above. Nobody wants to pass in 14 arguments to a
function, of which several will have sensible defaults. And I don't see much
reason to name a new struct if it's only going to be used for the one function.
</p>
<h2>Proposals for Named Arguments</h2>
<p>
First I'm going to describe some proposals that I've seen for improving named
arguments in Rust, that I don't like. Then I'll reveal what my preferred
solution is.
</p>
<h3>Default Field Values</h3>
<p>
This feature is already available in Nightly Rust. And the <code>syn</code>
create even recently added support for it. The idea is to make implementing
the <code>Default</code> trait easier by adding special syntax for it. It's
not proposed that this will solve named arguments by itself, but might be part
of a larger solution.
</p>
<pre>
#[derive(Default)]
struct Pet {
name: Option<String>, // impl Default for Pet will use Default::default() for name
age: i128 = 42, // impl Default for Pet will use the literal 42 for age
}
// Pet { name: Some(""), age: 42 }
let _ = Pet { name: Some(String::new()), .. };
// Compilation error: `name` needs to be specified
let _ = Pet { .. };
// Pet { name: None, age: 42 }
let _ = Pet::default();
</pre>
<p>
I don't actually have a problem with the proposal itself. I even created my
own library to implement a polyfill for it
(<a href="https://www.lib.rs/feluments">feluments</a>). It seems perfect for
structs. But it doesn't help very much for functions.
</p>
<h3>Structural Records</h3>
<p>
The RFC for default field values mentions a closed RFC for structural records.
They can be thought of as anonymous structs or tuples with named fields.
</p>
<pre>
fn do_stuff_with(color: { red: u8, green: u8, blue: u8 }) { // More ergonomic!
some_stuff(color.red); // *And* readable! :)
...
other_stuff(color.green);
...
yet_more_stuff(color.blue);
}
do_stuff_with({ red: 255, green: 127, blue: 63 });
</pre>
<p>
The RFC notes that this emulates named arguments, although it does not emulate
optional arguments. It wasn't a major motivation. The major motivation was to
get the convenience of tuples with the readability of structs.
</p>
<p>
Let's say for the sake of argument that this RFC succeeded. What would the
syntax of our "Hello, world" examples look like here? Let's assume default
field values are also merged, and they work with structural records.
</p>
<pre>
fn example(
required: String,
{requiredNamed, namedOptional}: {
requiredNamed: Option<String>,
namedOptional: String = "world".into()
}
): String {
format!("{required} {requiredNamed} {namedOptional}")
}
example("Hello", { requiredNamed: "beautiful", .. });
</pre>
<p>
Wow, that looks familiar. In fact it's the same syntax that TypeScript uses,
with all the problems that come with it. You need to specify the parameter
names twice.
</p>
<p>Structural records also come with many other challenges:</p>
<ul>
<li>
What happens with records that have only one field? <code>{ x }</code> can be
interpreted as either a block expression that returns <code>x</code>, or a
structural record expression: <code>{ x: x, }</code>. To fix the ambiguity,
the RFC proposed requiring an extra comma: <code>{ x, }</code>, like what
tuples do. But this requires making the Rust parser much more complicated,
to do several tokens of lookahead.
</li>
<li>
It's impossible to implement any traits on these. The RFC proposed magically
implementing several traits. And unlike for tuples, we can't just define some
generic trait implementations, because there can be so much variation in the
field names.
</li>
<li>
A newcomer to the language may mistake this syntax for a Python dictionary
or Dart's map type.
</li>
</ul>
<p>
Interestingly, TypeScript actually has named tuple fields. But they only exist
at the type declaration. You can't use the field names to access the fields.
</p>
<pre>
type NewLocation = [lat: number, long: number]
const newLocations: NewLocation[] = [
[52.3702, 4.8952],
[53.3498, -6.2603]
]
const firstLat = newLocations[0][0]
const firstLong = newLocations[0][1]
</pre>
<p>
Rust's language team decided that although something resembling this feature
seems useful, it would be very hard to implement and not worth the effort.
</p>
<h3>Copy C#</h3>
<p>
If we're already agreeing on a syntax for optional fields in structs, why not
just copy that over to functions? First, we can allow the arguments to be
specified in any order, as long as they are named, and no out-of-order
positional arguments are following a named argument.
</p>
<pre>
print_order_details(order_num: 31, product_name: "Red Mug", seller_name: "Gift Shop");
print_order_details(seller_name: "Gift Shop", product_name: "Red Mug", order_num: 31);
print_order_details("Gift Shop", 31, product_name: "Red Mug");
print_order_details(seller_name: "Gift Shop", 31, product_name: "Red Mug");
// this would cause an error
print_order_details(product_name: "Red Mug", 31, "Gift Shop");
</pre>
<p>
Then, we could define default values, and allow parameters with default values
to be unspecified.
</p>
<pre>
fn print_order_details(product_name: &str, order_num: 31, seller_name: &str = "Gift Shop");
print_order_details("Red Mug", 31);
</pre>
<p>
Unlike for the default field values proposal, we can't use a <code>..</code>
at the end here, because it is also a valid expression.
</p>
<p>
The biggest problem with this idea is that it makes parameter names part of
the public API of the function, without functions opting into it. There was
previously an objection on the grounds of it conflicting with the proposed
type ascription feature. But the RFC for it has since been deleted, so I don't
think we need to worry about that anymore.
</p>
<h3>Public Arguments</h3>
<p>
As far as I know, a formal RFC was never made for this, but a pre-RFC was
posted to the Rust Internals Forum several years ago, and it's the most
thorough proposal for named arguments that I know of. I'll avoid copy-pasting
the entire RFC here, but I'll give a high level overview.
</p>
<p>Here's an example of a function with named arguments from the RFC:</p>
<pre>
pub struct Database;
pub struct RegistrationError;
pub fn register(
pub name: String,
pub surname: String,
to db: Database
) -> Result<(), RegistrationError> {
/* ... */
}
register(name: "Alexis".into(), surname: "Poliorcetics".into(), to: my_db);
</pre>
<p>
In the case of <code>to db</code>, the name of the argument is <code>to</code>,
but it's referred to as <code>db</code> within the body of the function. For
the other two parameters, the <code>pub</code> keyword is used to say that
that both the name of the argument when calling the function, and the name of
the parameter in the function body, are the same.
</p>
<p>
The RFC goes into way more detail, proposing function overloads, using named
arguments with the <code>Fn</code> trait, exposing names from patterns, method
overloads, and explaining the documentation aspect.
</p>
<p>Here are the criticisms I saw in the comments:</p>
<ul>
<li>
Unordered named arguments was ruled out. I don't like the justification
RustyFrog gave for this, but it seems easy to support with this model.
</li>
<li>
Default parameter values also weren't proposed, even though it's the main
reason for wanting named parameters
</li>
<li>
Despite not proposing default parameter values, the pre-RFC did propose
overloads, which are very difficult to implement in Rust, and they're not as
useful as optional arguments. It's not proposing type-based overloads, which
is a bit better, but still.
</li>
<li>It's a very big RFC, which might make it difficult to get it approved.</li>
<li>
There are some syntax ambiguities, like <code>fn foo(name (a, b): name)</code>,
which is currently valid syntax. There was also a proposed type acription
feature that it conflicted with, but as I said before, that RFC has since
been removed.
</li>
<li>
Using <code>pub</code> might be confusing, given how it's used currently. I
don't think I mind that personally, because its effect is to make the name of
the parameter part of a function's public API. But I get how that's a bit
weird.
</li>
<li>
The proposal for changing the <code>Fn</code> trait seemed underbaked. The
<code>Fn(f32, f32) -> String</code> trait gets desugared to
<code>Fn<(f32, f32), Output = String></code>, but it's not clear what the
desugaring for named parameters should look like.
</li>
<li>
Despite explaining the usage for <code>Fn</code> implementations, the <code>fn</code>
type was not mentioned at all.
</li>
<li>
The pre-RFC was not clear about if named parameters were required to be used
by name, or if having the correct position was technically sufficient.
</li>
<li>
The handling of extern functions with named arguments is fairly complicated
to the point where it would probably be better to not handle them.
</li>
</ul>
<p>
My understanding is that the author got busy and didn't address these concerns,
but I think the pre-RFC serves as a good starting point.
</p>
<h3>Struct-style function calls</h3>
<p>
This idea was proposed back in 2016 by nixpulvis. I've also seen it floating
around in some other places.
</p>
<pre>
// 1. Tuple style method call (current functions).
fn foo(a: u32) {}
foo(2);
// 2. Struct style method call.
fn foo { a: u32 }
foo { a: 2 };
// SomeType { a: 1 } -> SomeType
// SomeFunc { a: 1 } -> Codomain
// generally_lowercase { a: 1 } -> Codomain
// 3. Together.
fn foo(a: u32) { a: u32 } {}
foo(1) { a: 2 };
</pre>
<p>
I personally don't like it because it makes struct constructions and function
calls look too similar. I also have no idea how the compiler would parse this.
But then again, most languages use <code>new Foo()</code> syntax for
constructors, so maybe somebody will like it.
</p>
<h3>Copy Dart</h3>
<p>
Another proposal that's come up, including from myself, is to copy the Dart
syntax, like so:
</p>
<pre>
fn foo({ foo: &str = "Hello", bar: &str = "World" }) -> String {
format!("{foo} {bar}")
}
</pre>
<p>
To me, this looks too much like existing pattern syntax. But I do like that it
doesn't require an extra keyword on each argument.
</p>
<h3>Edition Separation</h3>
<p>
Some people have proposed solving the backwards-compatibility problem by only
allowing named arguments to be used with functions that were written in a
future edition of Rust. To me, this feels like too large of a breaking change
to be worth considering.
</p>
<h3>Dot Prefixes</h3>
<p>
An RFC was opened in 2020 proposing using a dot prefix to distinguish between
named and unnamed variables. It was not merged, because the RFC was a very
incremental step that didn't explain the future plans for how the named
arguments would work. Here's an example from the PR:
</p>
<pre>
fn split(string: &str, .at: char, .limit: usize, .case_sensitive: bool) {}
split("hello world", .at = ' ', .limit = 2, .case_sensitive = true);
</pre>
<p>
The dot was chosen because it's only one character, (as opposed to four
characters, with the <code>pub</code> keyword plus a space). But I honestly
don't think it's intuitive enough. The dot is also reminds the user of struct
fields, which is a completely unrelated concept.
</p>
<h2>My Proposal</h2>
<p>
After reading all of these proposals, I feel that all of the pieces exist for
a complete named arguments proposal that lack any of the downsides. I get the
impression that some people are going to be opposed to the concept no matter
what, but I tried to address as many counter-arguments for named arguments as
possible in this post. The rest of this post will be a pre-RFC that think
will satisfy as many people as possible. I'll monitor the comments anywhere
this gets posted to see if there are any major criticisms. I can't guarantee
that I'll actually make an RFC, but permission is granted to create an RFC if
I don't. I think it's been a while since named arguments were last seriously
proposed, so I think now is a good time for a new proposal.
</p>
<h3>Summary</h3>
<p>
Add named and optional arguments to functions. Functions can have both
positional and named arguments. In function calls, named arguments may be
referred to by name, to increase readability and maintainability. Named
arguments may also have default values, avoiding the need to specify them in
the function call.
</p>
<h3>Motivation</h3>
<h4>Improve safety and readability</h4>
<p>
Misremembering which parameter is at which position is a major source of bugs
in many languages, including Rust. Take the function from the standard
library: <code><a href="https://doc.rust-lang.org/stable/std/fs/fn.hard_link.html">hard_link</a></code>.
</p>
<pre>
pub fn hard_link<P: AsRef<Path>, Q: AsRef<Path>>(
original: P,
link: Q,
) -> Result<()>
</pre>
<p>
Both parameters accept the same types, so there's no way to know which one is
which without looking at the documentation. The consequence of misremembering
is a runtime error or a bug.
</p>
<p>
With named parameters, it is easy to see what is happening at the call-site.
This also makes the resulting code more readable.
</p>
<pre>
hard_link(original: "a.txt", link: "b.txt")?;
</pre>
<p>The effect is more pronounced for functions which have more parameters.</p>
<pre>
// Without named arguments: what are these numbers?
solar_elevation(1787517098.0, 38.897957, -77.036560)
// With named arguments, it's obvious
solar_elevation(timestamp: 1787517098.0, latitude: 38.897957, longitude: -77.036560);
</pre>
<p>
A similar effect is possible in Rust today, by creating a new struct and
having that be the function's only parameter. But it's very verbose to create,
so in practice, few libraries take advantage of it. Importantly, the standard
library rarely uses this pattern.
</p>
<h4>Boilerplate reduction</h4>
<p>
Many functions have reasonable defaults that should be passed into many of
their parameters, but because all positional arguments are required, all of
the parameters must be specified. Passing <code>None</code> into every
parameter comes with readability issues.
</p>
<pre>
repository.checkout_index(None, None)?;
</pre>
<p>
Passing a struct into the parameters does give us names, but still requires
us to specify <code>None</code> for the optional values. Sometimes this
boilerplate can be reduced by adding <code>..Default::default()</code> to the
end of the constructor, <code>Default</code> cannot be implemented if any of
the fields are not optional. Implementing <code>Default</code> is also a lot
of boilerplate if it cannot be implemented simply by using the derive macro.
Some of these problems can be addressed by
<a href="https://github.com/rust-lang/rfcs/pull/3681">RFC 3681</a>, but it
still requires a new struct to be defined specifically for one function.
</p>
<pre>
let instance = Instance::new(InstanceDescriptor {
backends: Backends::default(),
flags: InstanceFlags::default(),
memory_budget_thresholds: MemoryBudgetThresholds::default(),
backend_options: BackendOptions::default(),
display: None,
});
</pre>
<p>
To help reduce the boilerplate of calling such a function, many libraries
implement builder types that don't require every value to be specified. This
comes at the cost of even more boilerplate to create the builder, which
becomes part of the library's public API. There are crates, such as
<a href="https://www.crates.io/crates/bon">bon</a> to make declaring a builder
for a struct easier, but it still requires a struct be declared for a single
function. Libraries don't always use these crates either, as doing so
increases compile times. The <code>Command</code> struct uses 60 SLOCs (and
many more lines of documentation) to allow us to do this:
</p>
<pre>
Command::new("printenv")
.stdin(Stdio::null())
.stdout(Stdio::inherit())
.env_clear()
.envs(&filtered_env)
.spawn()
.expect("printenv failed to start");
</pre>
<p>If we had named arguments, the following function would be easy to define:</p>
<pre>
spawn(
command: "printenv",
stdin: Stdio::null(),
stdout: Stdio::inherit(),
env_clear: true,
envs: &filtered_env
).expect("printenv failed to start");
</pre>
<p>
Bon also provides utilities for emulating named arguments in functions, but
calling such a function involves some boilerplate as well.
</p>
<pre>
// Example from the bon README
#[builder]
fn greet(name: &str, level: Option<u32>) -> String {
let level = level.unwrap_or(0);
format!("Hello {name}! Your level is {level}")
}
// This could be shortened to greet(name: "Bon", level: 24)
let greeting = greet()
.name("Bon")
.level(24)
.call();
assert_eq!(greeting, "Hello Bon! Your level is 24");
</pre>
<p>
In practice, the added complexity means that these patterns are only ever used
in public APIs, so private functions have no worthwhile workaround to improve
readability.
</p>
<h4>Extensibility without breaking changes</h4>
<p>
Currently, adding a new parameter to an existing function is a breaking
change. If you want to add a new field to a struct, your existing factory must
implement a reasonable default value, and a new function must be created to
enable the new functionality.
</p>
<p>
This leads to some APIs having many functions which do similar things, but
with different parameters. For example, <code>HashMap</code> from the standard
library has the following factories.
</p>
<pre>
impl<K, V> HashMap<K, V, RandomState> {
pub fn new() -> HashMap<K, V, RandomState>;
pub fn with_capacity(capacity: usize) -> HashMap<K, V, RandomState>;
}
impl<K, V, A: Allocator> HashMap<K, V, RandomState, A> {
pub fn new_in(alloc: A) -> Self;
pub fn with_capacity_in(capacity: usize, alloc: A) -> Self;
}
impl<K, V, S> HashMap<K, V, S> {
pub const fn with_hasher(hash_builder: S) -> HashMap<K, V, S>;
pub fn with_capacity_and_hasher(capacity: usize, hasher: S) -> HashMap<K, V, S>;
}
impl<K, V, S, A: Allocator> HashMap<K, V, S, A> {
pub fn with_hasher_in(hash_builder: S, alloc: A) -> Self;
pub fn with_capacity_and_hasher_in(capacity: usize, hasher: S, alloc: A) -> Self;
}
</pre>
<p>
All of these functions are shown in the documentation, polluting the API. The
number of functions is exponential with respect to the number of parameters.
If we ignore the differences in generic parameters (a possible solution to
which is discussed in the "Future possibilities" section), then we could
imagine having a single function.
</p>
<pre>
pub fn new(
pub capacity: usize = 0,
pub alloc: A = Global,
pub hasher: S = RandomState,
);
</pre>
<p>
If we wanted to some day add a <code>fill</code> parameter, we'd be able to
add it to this existing function without it being a breaking change, and
without doubling the number of factories for <code>HashMap</code>.
</p>
<h3>Guide-level explanation</h3>
<p>
Parameters may be named or unnamed. By default, all parameters to a function
are unnamed, and cannot be referred to by name. We can create a named
parameter by adding the <code>pub</code> keyword before its name.
</p>
<pre>
fn print_labeled_measurement(pub value: i32, pub unit_label: char) {
println!("The measurement is: {value}{unit_label}");
}
fn main() {
print_labeled_measurement(5, 'h');
}
</pre>
<p>
At first, this may not seem very different from unnamed parameters. But when
a parameter is named, we may use its name when calling the function. This
makes it easier to tell, at a glance, what the values being passed into the
function are meant to do.
</p>
<pre>
print_labeled_measurement(value: 5, unit_label: 'h');
</pre>
<p>
When parameters are named, they may also be re-ordered, so you don't need to
remember the position of each one.
</p>
<pre>
print_labeled_measurement(unit_label: 'h', value: 5);
</pre>
<p>
There are some caveats to the ordering. A function may contain both named and
unnamed parameters. But if it does, all named arguments must be specified
after the positional arguments, both in the function declaration and in the
function body.
</p>
<pre>
// unit_label is named, value is not
fn print_labeled_measurement(value: i32, pub unit_label: char) {
println!("The measurement is: {value}{unit_label}");
}
// forbidden: all named parameters must be after all unnamed parameters
fn _print_labeled_measurement(pub value: i32, unit_label: char) {
// ...
}
fn main() {
// forbidden: the parameter that 'h' is being passed into must be named
print_labeled_measurement(value: 5, 'h');
// forbidden
print_labeled_measurement(unit_label: 'h', 5);
}
</pre>
<p>
Named parameters may also have default values. This means that the caller does
not need to specify the argument value when calling the function. The default
value is using automatically.
</p>
<pre>
fn print_labeled_measurement(pub value: i32, pub unit_label: char = 'm') {
println!("The measurement is: {value}{unit_label}");
}
fn main() {
// unit_label is unspecified and is defaulted to 'm'
print_labeled_measurement(5)
}
</pre>
<p>
Named parameters cannot be used if the parameter does not have a name and is
defined as a pattern instead. For example, the following is not valid.
</p>
<pre>
fn print_coordinates(pub &(x, y): &(i32, i32)) {
// ...
}
</pre>
<p>
However, a name can be given to this parameter by using an <code>@</code>
binding.
</p>
<pre>
fn print_coordinates(pub point @ &(x, y): &(i32, i32)) {
// ...
}
</pre>
<p>
Traits may also choose to use named parameters, in which case the
implementation must use the same name. However, trait function parameters
cannot have default values.
</p>
<pre>
trait Trait {
fn f(pub a: i32, pub b: i32);
}
impl Trait for () {
// This is okay, because the parameter names match
fn f(pub a: i32, pub b: i32) {}
}
impl Trait for i32 {
// invalid: the second argument is named `b` in the definition
fn f(pub a: i32, pub c: i32) {}
}
impl Trait for bool {
// invalid: `b` is not public
fn f(pub a: i32, b: i32) {}
}
impl Trait for X {
// invalid: trait methods cannot have default parameter values
fn f(pub a: i32, pub b: i32 = 0) {}
}
</pre>
<p>
Because the parameter names are optional, function pointers may be created for
functions with named parameters. However, the name will not be accessible on
the function pointer, and default values cannot be used. This also applies for
the <code>Fn*</code> family of traits.
</p>
<pre>
fn foo(pub a: i32, pub b: i32) {}
let f: fn(i32, i32) = foo;
f(4, 2); // ok
f(a: 4, b: 2); // ERROR! `f` can't be called with named arguments
fn higher_order(f: Fn(i32, i32)) {
f(4, 2); // ok
f(a: 4, b: 2); // ERROR! `f` can't be called with named arguments
}
higher_order(foo); // ok
higher_order(|_, _| {}); // ok
</pre>
<h3>Reference-level explanation</h3>
<h4>Grammar</h4>
<p>
The only needed change to support public parameters in function declarations
is to function parameters. First, we add the <code>pub</code> keyword, which
must be followed by an identifier pattern. Such a pattern may also include a
default value.
</p>
<pre>
FunctionParamPattern = PatternNoTopAlt ":" ( Type | "..." )
| "pub" IdentifierPattern ":" ( Type | "..." ) ( "=" Expr )?
</pre>
<p>
For function calls, the syntax is slightly more complicated to parse, but only
affects <code>CallParams</code>.
</p>
<pre>
CallParam = Expression ( ":" Expression )?
CallParams = CallParam ( "," CallParam )* ","?
</pre>
<p>
This is slightly more complicated to parse, because an identifier is also a
valid expression. To solve this, we'll just parse an entire expression, and
then check to see if there's a colon following it. The compiler can later
give an error if the argument name is anything other than an identifier.
</p>
<h4>Static semantics</h4>
<h5>Function parameters</h5>
<p>
Given a <code>FunctionParamPattern</code> where the default is specified, i.e.:
</p>
<pre>
FunctionParamPattern = "pub" pat:IdentifierPattern ":" ( ty:Type | "..." ) ( "=" expr:Expr )
</pre>
<p>Similar rules to the rules for default field values apply. Namely,</p>
<ul>
<li>
If the function is <code>const</code>, then the expression, <code>expr</code>
must be a constant expression.
</li>
<li>The expression <code>expr</code> must coerce to the type <code>ty</code></li>
<li>
Generic parameters of the current items are accessible.
<pre>
fn foo<const A: usize>(bar: usize = A) {}
</pre>
</li>
<li>
Default const expressions are not evaluated at definition time, only during
instantiation. This means that the following will not fail to compile:
<pre>
fn foo(a: usize = panic!(), b: usize = 42) {}
foo(a: 0);
</pre>
</li>
<li>
The function's generic parameters are properly propagated, meaning the following is possible:
<pre>
fn foo<T>(bar: Vec<T> = Vec::new()) {}
foo::<T>();
</pre>
</li>
<li>
When lints check attributes such as <code>#[allow(lint_name)]</code> are
placed on a <code>FunctionParam</code>, it also applies to the expression,
<code>expr</code>.
</li>
<li>Unused parameter names will not emit a warning.</li>
</ul>
<p>
For public parameters in general, all public parameters must be strictly after
all non-public parameters.
</p>
<h5>Function calls</h5>
<p>
The semantics of function calls change significantly. In general, all named
arguments must be specified after all positional arguments. For positional
arguments, all existing semantics apply.
</p>
<p>In the case of the following production of a named argument:</p>
<pre>
CallParam = name:Expression ":" value:Expression
</pre>
<p>the following rules apply</p>
<ul>
<li>The expression, <code>name</code> must be a plain identifier.</li>
<li>
The identifier, <code>name</code> must be the name of a public parameter of
the function.
</li>
<li>
The expression, <code>value</code> must coerce the the type of the
aforementioned public parameter.
</li>
</ul>
<h5>Trait declarations</h5>
<p>
For trait declarations all the same rules apply from function declarations. In
addition:
</p>
<ul>
<li>Trait methods cannot have default parameter values.</li>
</ul>
<h5>Trait implementations</h5>
<p>
The semantics of trait implementations vary slightly from the semantics for
trait declarations.
</p>
<ul>
<li>
Any parameter which was declared public on the trait declaration must also
be public in the trait implementation.
</li>
<li>Public parameters must have the same name that their declaration has.</li>
</ul>
<h3>Drawbacks</h3>
<h4>Added complexity</h4>
<p>
The language would be slightly more complex as a result of this feature. But
this proposal is fairly minimal as far as named arguments proposals go. The
syntax for named arguments resembles the syntax for struct fields, and the
default value syntax is pretty similar to the syntax proposed for default
field values.
</p>
<p>
One concern that can come up in named argument proposals is the problem of it
only applying to new functions, which makes it hard to remember which
libraries. This proposal can be backported to the standard library, making the
language feel more consistent.
</p>
<h4>Named arguments aren't mandatory</h4>
<p>
The fact that names are optional could be seen as a compromise on the safety
benefits. Some people may lazily choose to leave them out, in which case, no
safety benefit is provided.
</p>
<p>
However, the fact that they aren't mandatory allows more library authors to
use them without worrying about breaking changes. Some authors may also be
more inclined to support named arguments if they felt confident that it would
not increase the verbosity of calling the function.
</p>
<p>
This drawback does present a potential bug. If the names are removed during a
refactor, then they may be in the wrong position after the refactor, causing
a bug.
</p>
<pre>
// before refactor
coordinates(y: 45.6, x: 6784.0);
// after refactor: incorrect order
coordinates(y, x);
</pre>
<h5>Remedy: Add a clippy lint</h5>
<p>
A clippy lint can be used to enforce the use of named arguments when possible.
</p>
<h5>Remedy: Require named arguments in a future edition</h5>
<p>
Although it may be a large breaking change, future editions could likely
require the use of named parameters, if it were deemed desirable to do so.
</p>
<h4>Limiting type ascription</h4>
<p>
Many older proposals for named arguments noted that the syntax chosen here:
<code>name: value</code> conflicts with the proposed type ascription feature.
However, since then, the RFC for type ascription has been removed. And another
type ascription RFC was rejected, partially on the basis of it potentially
conflicting with named arguments. So this is not likely to be a concern
anymore.
</p>
<h3>Rationale and alternatives</h3>
<h4>Why make named arguments opt-in?</h4>
<p>
Although many languages, such as Kotlin and C#, allow any argument to be
named, this is not practical for Rust. Changing an argument name is currently
not a breaking change, but would become one as soon as named arguments are
introduced. The solution is to allow authors to decide if their argument names
are an implementation detail or not.
</p>
<h4>The <code>pub</code> keyword</h4>
<p>
Using the <code>pub</code> keyword may seem like a strange choice, given the
semantics. But it has essentially the effect of making the parameter name
public, so this is defensible. Other visibility modifiers are not proposed by
this RFC, because it's hard to imagine a situation where a public function
would want to allow named arguments internally, but not externally.
</p>
<p>
Other modifiers have been proposed. One could imagine adding an <code>ext</code>
keyword to define named parameters. A previous RFC proposed using dots, but
this was deemed to be too reminiscent of field access. Other symbols, such as
<code>@</code> may be more acceptable. Another proposed idea is to copy Swift
syntax and use two identifiers.
</p>
<pre>
fn foo(bar bar: &str) {}
</pre>
<p>
Re-typing the same parameter name twice is verbose and usually unnecessary.
It's also unnecessary since syntax exists for rebinding a variable already.
</p>
<p>
Dart wraps named arguments in curly braces. This has the benefit of not
requiring the <code>pub</code> keyword to be specified multiple times. However,
in the context of Rust, this looks like a destructuring pattern, which isn't
semantically correct.
</p>
<pre>
// In Dart
void enableFlags({bool? bold, bool? hidden})
enableFlags(bold: true)
// Similar idea, in Rust
fn enable_flags({ bold: Option<bool> = None, hidden: Option<bool> = None })
enable_flags(bold: Some(true))
</pre>
<p>
Another proposal is to make function calls with named arguments look more like
struct constructions by using curly braces instead of parentheses. This seems
like it could be very confusing and make the distinction between the two
features unclear.
</p>
<pre>
fn foo (a: u32) { b: u32 } {}
foo(1) { b: 2 };
</pre>
<h4>Colon vs equal sign</h4>
<p>
Some have proposed using an equal sign (<code>=</code>) instead of a colon
(<code>:</code>). This would make the syntax more closely resemble assignment,
which is semantically true. It also would not possibly conflict with type
ascription.
</p>
<p>
The colon syntax was chosen for this RFC to be more consistent with struct
initializers.
</p>
<h4>Argument re-ordering</h4>
<p>
Some proposals for named arguments have omitted re-ordering of arguments for
simplicity. For the sake of thoroughness, this RFC proposes allowing arguments
to be re-ordered. A possible alternative would be to require arguments to
still be in the original order. The drawback would be unnecessary friction
when the user sorts the arguments out of order.
</p>
<p>
Some languages allow for more flexible re-ordering than this RFC proposes. C#
allows named arguments to appear before unnamed arguments, as long as the
arguments are still in order. This RFC omits this feature, in order to make
named arguments seem less confusing to users. The reordering proposed here
still allows for more flexible argument-reordering in the future.
</p>
<h4>No default values in traits</h4>
<p>
This RFC currently prohibits traits from making use of default parameter
values. This is because it's unclear if default values for trait parameters
should be defined at the trait definition or the trait implementation. It
would likely be more predictable if default values could be defined by the
implementation. But it's hard to imagine how a default value would allow the
trait to be dyn-compatible. If the default values were defined at the trait
definition, then they could stay dyn-compatible.
</p>
<h4>On const contexts</h4>
<p>
In the default field values RFC, it was decided that default values for
structs must be const. The reasoning is that functions with side-effects may
be confusing for the compiler and for users. However, these justifications
don't apply to functions, because non-const functions don't carry an
expectation of being cheap or consistent. It is possible to always require
default values to be const, but there's seemingly no strong reason to do so.
</p>
<p>
However, it would be confusing if default parameters could be non-const in
const functions, which is why they are required to be const in that case.
</p>
<h4>Named arguments aren't mandatory</h4>
<p>See the section above in "Drawbacks".</p>
<h4>Alternative: Do nothing</h4>
<p>
The "Motivation" section explains thoroughly how the current state is very
verbose, to the point where existing alternatives, such a struct parameters
and builders are often not used. The existence of these patterns is evidence
that users want some way to emulate named arguments. The fact that these
alternatives are not used in private APIs is evidence that the status quo is
annoyingly verbose.
</p>
<h4>Alternative: Structural Records</h4>
<p>
It has been proposed many times that structural records, alongside default
field values, could emulate named parameters. This is a pattern that is often
used in languages like TypeScript. The syntax for declaring such a function
would look like this.
</p>
<pre>
fn foo({ a, b, c }: { a: f64, b: f64, c: f64 }) {}
</pre>
<p>There are several criticisms I have of this pattern:</p>
<ul>
<li>
It cannot be backported to older functions without causing a breaking change,
so the standard library could not use it.
</li>
<li>
Every identifier in this declaration needs to be typed twice. It might be
possible to introduce syntactic sugar for this, but not such syntax has been
proposed.
</li>
<li>
Structural records have had a proposed RFC in the past, but was rejected,
mainly due to the complexity of implementing it.
</li>
</ul>
<p>
Structural records could serve as a temporary compromise for named arguments,
if they are ever implemented. I'm not currently convinced that they ever will
be implemented. If the compromise solution is harder to implement than the
real solution, then we might as well just use the real solution.
</p>
<h4>Alternative: Struct name inference</h4>
<p>
One proposal to make named arguments easier is to allow the name of a struct
to be inferred when it is constructed. This would make the call sites of
functions using struct parameters to look mostly simple.
</p>
<pre>
foo(_ { a: 1, b: "2", c: [] });
</pre>
<p>
However, this still requires the function to support this function explicitly,
by defining a struct to be passed in. It also still cannot be backported to
existing functions. The only benefit this provides over the status quo is to
avoid typing the name of the struct at the call site, which is not the main
criticism of this pattern. It would still be unlikely for private functions to
use this pattern.
</p>
<h4>Alternative: Implementing <code>Fn</code> traits</h4>
<p>
I've seen this come up a couple of times, but I think most of the people
suggesting this are doing so as a joke. Implementing <code>Fn</code> would
be a more verbose form of the method overload feature that some other
languages have. Overloads are difficult and controversial to implement in
Rust, so it's unlikely this pattern would be adopted.
</p>
<h3>Prior art</h3>
<p>
Several previous attempts at named arguments, both in Rust and in other
languages, have been described throughout this RFC.
</p>
<p>
A previous RFC for named arguments exists for Rust. It was rejected mainly
because it did not describe what the future of named arguments would look like
with optional arguments. This RFC attempts to address as many future uses of
named arguments as possible in order to give a full picture.
</p>
<p>
A pre-RFC was proposed by forum user Azerupi, but no RFC seemed to come as a
result of it. Several complications were pointed out by commenters. This RFC
attempts to address syntactic ambiguity, the type system, and opt-in naming.
</p>
<p>
Another pre-RFC proposed using a more Swift-style syntax, which was avoided in
this RFC. The pre-RFC also proposed overloading but not default arguments.
Arguably the latter is more important than the former.
</p>
<p>
As was mentioned before, bon offers a procedural macro for creating builders
from functions. The existence of this feature shows that there is a desire for
named arguments, that would be better handled as a language feature.
</p>
<p>
Other lanaguages with named arguments have also been discussed. The feature
proposed here is similar to named arguments from C# or Kotlin, with the
difference being that thhe named arguments are opt-in. The syntaxes used by
Dart and Swift have also been discussed. Dart uses the following syntax, which
looks closer to destructuring than named arguments.
</p>
<pre>
void enableFlags({bool? bold, bool? hidden})
enableFlags(bold: true)
</pre>
<p>
Swift makes all arguments named by default, allowing authors to opt-out by
using an underscore before the variable name. This is impossible to port to
Rust without making a breaking change to the language. Various proposals have
tried to use slightly different syntaxes, but many of them present syntactic
ambiguities.
</p>
<h3>Unresolved questions</h3>
<ul>
<li>Should we require named arguments to be given in position order?</li>
<li>Is <code>pub</code> the best keyword, or should a different keyword be used?</li>
<li>Should default values be required to always be const?</li>
</ul>
<h3>Future possibilities</h3>
<h4>Default parameter values for non-public parameters</h4>
<p>
In this proposal, only public parameters can have default values. Optional
named parameters are much more flexible than optional positional parameters,
but many languages do still support optional positional arguments. It's not
hard to imagine extending this proposal to positional arguments, although the
use-case would be more limited.
</p>
<h4>Default parameter values for trait methods</h4>
<p>
Trait methods were forbidden from having default values due to the question of
if the default value should be on the trait declaration or the trait
implementation. Having it be on the implementation would be consistent with
how associated types and constants work. But this would make it hard for such
a trait to be dyn-compatible.
</p>
<p>
One solution would be to only allow default values to be used in non-dyn
contexts.
</p>
<pre>
trait Foo {
fn bar(&self, baz: i32 = 0);
}
impl Foo for i32 {
fn bar(&self, baz: i32 = i32::MAX) {}
}
fn do_bar<T: Foo>(x: &T) {
x.bar(); // baz doesn't need to be specified, since we're using generics
}
fn do_bar_2(x: &dyn T) {
x.bar(); // ERROR: baz must be specified when using a method on a dyn object
}
</pre>
<h4>Extending traits</h4>
<p>
Theoretically, there's nothing preventing the following, as long as a default
parameter value is provided:
</p>
<pre>
impl<T> Default for Vec<T> {
fn default(capacity: usize = 0) -> Self {
Self::with_capacity(capacity)
}
}
</pre>
<p>
Although, in addition to making the behavior of the <code>default</code>
function inconsistent, this is even harder to resolve in a <code>dyn</code>
context. The likely remedy would be to forbid <code>Vec</code> from being
converted into a <code>dyn Default</code>, which would make introducing the
named parameter a breaking change.
</p>
<h4>Requiring named arguments in a future edition</h4>
<p>
Allowing named arguments to be optional is necessary in order to backport them
into existing functions without creating breaking changes. This comes with
some compromises, as has been described in the "Drawbacks" section. However,
the way this RFC is drafted makes it possible to require named arguments in a
future edition of Rust. Specifying a named argument would not give a warning
in either the previous edition or the new edition.
</p>
<p>
Doing this too early would likely hurt adoption of named arguments, as
backporting them into existing functions would become a breaking API change.
This would also increase the verbosity of calling functions which make use of
named parameters, hurting adoption further.
</p>
<h4>More flexible re-ordering</h4>
<p>
As mentioned before, re-ordering of named arguments is allowed by this RFC,
in a limited way. The limitations are kept in order to keep the rules easy to
explain and reason about. However, some languages allow arguments to be
re-ordered more flexibly, and this would be possible to do in the future.
</p>
<p>
C#'s main rule is that out-of-order named arguments cannot be followed by
positional arguments.
</p>
<pre>
print_order_details(order_num: 31, product_name: "Red Mug", seller_name: "Gift Shop");
print_order_details(seller_name: "Gift Shop", product_name: "Red Mug", order_num: 31);
print_order_details("Gift Shop", 31, product_name: "Red Mug");
print_order_details(seller_name: "Gift Shop", 31, product_name: "Red Mug");
// this would cause an error
print_order_details(product_name: "Red Mug", 31, "Gift Shop");
</pre>
<h4>Defaults Affect Inference</h4>
<p>
In 2022, Gankra proposed using default parameter values as part of inference
for generic type parameters. We could imagine something like this.
</p>
<pre>
fn default_hasher() -> RandomState { ... }
impl <K, V, S: BuildHasher> HashMap<K, V, S> {
fn new(hasher: S = RandomState::default()) { ... }
}
// uses S=RandomState
let map = HashMap::new();
</pre>
<p>
The downside of this particular approach, as Gankra pointed out, is that this
would only allow the argument to be omitted if the type of <code>S</code> is
<code>RandomState</code>. Although, that's already true with the current set
of <code>HashMap</code> factories, unless you use <code>default</code> which
doesn't have any parameters at all.
</p>
<h4>Generic const</h4>
<p>
In the case of <code>Vec</code>, adding an optional <code>capacity</code>
parameter is not possible with this iteration of the RFC, because
<code>Vec::new</code> is a <code>const</code> function, and allocating memory
cannot be done at compile-time. This is a very difficult problem to solve,
and it's possible that it never will. However, if keyword generics are added
to the language, it may be possible to do something like this:
</p>
<pre>
const<C> trait Capacity: Copy {
const<C> fn allocate<T>(self) -> Option<Box<[MaybeUninit<T>]>>;
const fn as_usize(self) -> usize;
}
const impl Capacity for () {
const fn allocate<T>(self) -> Option<Box<[MabyUninit<T>]>> {
None
}
const fn as_usize(self) -> usize {
0
}
}
impl Capacity for usize {
fn allocate<T>(self) -> Option<Box<[MaybeUninit<T>]>> {
Some(Box::default())
}
const fn as_usize(self) -> usize {
self
}
}
const<C> impl<T, Cap: const<C> Capacity> Vec<T> {
pub const<C> fn new(capacity: Cap = ()) -> Self {
Self {
len: 0,
capacity: capacity.as_usize(),
buffer: capacity.allocate(),
}
}
}
</pre>
<p>
If no argument value is provided, then the function would be
<code>const</code>. But when a <code>usize</code> is provided, then it would
no longer be <code>const</code>.
</p>
</article>
</body>
|