From 5ec2d93446ae890e164dd8ad33602a09c0d8814e Mon Sep 17 00:00:00 2001
From: Mica White
Date: Sat, 5 Sep 2026 21:29:35 -0400
Subject: First commit
---
src/blog/named-optional-args.kuht | 1701 +++++++++++++++++++++++++++++++++++++
1 file changed, 1701 insertions(+)
create mode 100644 src/blog/named-optional-args.kuht
(limited to 'src/blog/named-optional-args.kuht')
diff --git a/src/blog/named-optional-args.kuht b/src/blog/named-optional-args.kuht
new file mode 100644
index 0000000..7d36972
--- /dev/null
+++ b/src/blog/named-optional-args.kuht
@@ -0,0 +1,1701 @@
+
+
+
+ Named and Optional Arguments are Awesome
+
+
+
+
+
+
+
+Named and Optional Arguments are Awesome
+
+
+ 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.
+
+
+
+ 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.
+
+
+How Other Languages Handle Named Parameters
+
+Dart
+
+
+ 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.
+
+
+
+ First, let's look at a normal function, just to familiarize ourselves with the
+ language:
+
+
+
+Widget Text(String text) {
+ // ...
+}
+
+
+
+ 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.
+
+
+
+ Now, let's add a named parameter to set the size of the text, and default it to
+ 16 pixels:
+
+
+
+Widget Text(String text, {int fontSize = 16}) { /* ... */ }
+
+// usage:
+Text("Hello, world!", fontSize: 24)
+
+
+
+ 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.
+
+
+
+Widget Text({
+ int fontSize = 16,
+ Color? color,
+ required String text,
+});
+
+
+
+ Dart also supports optional positional arguments by wrapping the parameter in
+ square brackets.
+
+
+
+// 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');
+
+
+C#
+
+
+ C#'s handling of named parameters is also pretty good. In fact, any parameter can be
+ named.
+
+
+
+void ExampleMethod(string foo, string bar)
+{
+ Console.WriteLine($"{foo} {bar}")
+}
+
+ExampleMethod(bar: "Hello", foo: "world");
+
+
+
+ And any parameter can have a default value, as long as they're specified after
+ the required parameters.
+
+
+
+void ExampleMethod(string foo, string bar = "world")
+{
+ Console.WriteLine($"{foo} {bar}")
+}
+
+ExampleMethod("Howdy");
+
+
+And that's all there is to it. I like the simplicity of it.
+
+TypeScript
+
+
+ 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.
+
+
+
+function example(required: string, optional?: string = "world"): string {
+ return `${required} ${optional}`;
+}
+
+example("Hello") === "Hello world";
+example("Hello", "TypeScript") === "Hello TypeScript";
+
+
+
+ That seems fine to me. If you don't specify the default value, then it gets
+ set to undefined. 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.
+
+
+
+function example(
+ required: string,
+ {
+ requiredNamed,
+ namedOptional = "world"
+ }: { requiredNamed: string, namedOptional?: string }
+): string {
+ return `${required} ${requiredNamed} ${namedOptional}`;
+}
+
+example("Hello", { requiredNamed: "beautiful" });
+
+
+
+ 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: requiredNamed and namedOptional. Then
+ we destructure this parameter so we can use the fields in the function.
+
+
+
+ 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.
+
+
+
+ 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.
+
+
+Rust
+
+
+ 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 Default trait, so that
+ we can end the struct initializer with ..Default::default().
+
+
+
+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()
+);
+
+
+
+ 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 String),
+ 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.
+
+
+Why Rust Needs Optional Parameters
+
+
+ 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.
+
+
+Take some of the factories of HashMap, for example.
+
+
+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;
+}
+
+
+
+ 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.
+
+
+
+ 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.
+
+
+
+fn print(text: &str, bold: bool, italics: bool, underline: bool);
+
+
+
+ It's easy to mix up the parameters here because they don't have names. If I
+ forget that the italics parameter is third and not second, then I
+ can end up accidentally bolding my text instead.
+
+
+
+ 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.
+
+
+Proposals for Named Arguments
+
+
+ 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.
+
+
+Default Field Values
+
+
+ This feature is already available in Nightly Rust. And the syn
+ create even recently added support for it. The idea is to make implementing
+ the Default 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.
+
+
+
+#[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();
+
+
+
+ I don't actually have a problem with the proposal itself. I even created my
+ own library to implement a polyfill for it
+ (feluments). It seems perfect for
+ structs. But it doesn't help very much for functions.
+
+
+Structural Records
+
+
+ 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.
+
+
+
+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 });
+
+
+
+ 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.
+
+
+
+ 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.
+
+
+
+fn example(
+ required: String,
+ {requiredNamed, namedOptional}: {
+ requiredNamed: Option<String>,
+ namedOptional: String = "world".into()
+ }
+): String {
+ format!("{required} {requiredNamed} {namedOptional}")
+}
+
+example("Hello", { requiredNamed: "beautiful", .. });
+
+
+
+ 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.
+
+
+Structural records also come with many other challenges:
+
+ -
+ What happens with records that have only one field?
{ x } can be
+ interpreted as either a block expression that returns x, or a
+ structural record expression: { x: x, }. To fix the ambiguity,
+ the RFC proposed requiring an extra comma: { x, }, like what
+ tuples do. But this requires making the Rust parser much more complicated,
+ to do several tokens of lookahead.
+
+ -
+ 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.
+
+ -
+ A newcomer to the language may mistake this syntax for a Python dictionary
+ or Dart's map type.
+
+
+
+
+ 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.
+
+
+
+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]
+
+
+
+ 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.
+
+
+Copy C#
+
+
+ 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.
+
+
+
+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");
+
+
+
+ Then, we could define default values, and allow parameters with default values
+ to be unspecified.
+
+
+
+fn print_order_details(product_name: &str, order_num: 31, seller_name: &str = "Gift Shop");
+
+print_order_details("Red Mug", 31);
+
+
+
+ Unlike for the default field values proposal, we can't use a ..
+ at the end here, because it is also a valid expression.
+
+
+
+ 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.
+
+
+Public Arguments
+
+
+ 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.
+
+
+Here's an example of a function with named arguments from the RFC:
+
+
+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);
+
+
+
+ In the case of to db, the name of the argument is to,
+ but it's referred to as db within the body of the function. For
+ the other two parameters, the pub 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.
+
+
+
+ The RFC goes into way more detail, proposing function overloads, using named
+ arguments with the Fn trait, exposing names from patterns, method
+ overloads, and explaining the documentation aspect.
+
+
+Here are the criticisms I saw in the comments:
+
+ -
+ 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.
+
+ -
+ Default parameter values also weren't proposed, even though it's the main
+ reason for wanting named parameters
+
+ -
+ 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.
+
+ - It's a very big RFC, which might make it difficult to get it approved.
+ -
+ There are some syntax ambiguities, like
fn foo(name (a, b): name),
+ 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.
+
+ -
+ Using
pub 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.
+
+ -
+ The proposal for changing the
Fn trait seemed underbaked. The
+ Fn(f32, f32) -> String trait gets desugared to
+ Fn<(f32, f32), Output = String>, but it's not clear what the
+ desugaring for named parameters should look like.
+
+ -
+ Despite explaining the usage for
Fn implementations, the fn
+ type was not mentioned at all.
+
+ -
+ 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.
+
+ -
+ The handling of extern functions with named arguments is fairly complicated
+ to the point where it would probably be better to not handle them.
+
+
+
+
+ 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.
+
+
+Struct-style function calls
+
+
+ This idea was proposed back in 2016 by nixpulvis. I've also seen it floating
+ around in some other places.
+
+
+
+// 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 };
+
+
+
+ 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 new Foo() syntax for
+ constructors, so maybe somebody will like it.
+
+
+Copy Dart
+
+
+ Another proposal that's come up, including from myself, is to copy the Dart
+ syntax, like so:
+
+
+
+fn foo({ foo: &str = "Hello", bar: &str = "World" }) -> String {
+ format!("{foo} {bar}")
+}
+
+
+
+ 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.
+
+
+Edition Separation
+
+
+ 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.
+
+
+Dot Prefixes
+
+
+ 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:
+
+
+
+fn split(string: &str, .at: char, .limit: usize, .case_sensitive: bool) {}
+
+split("hello world", .at = ' ', .limit = 2, .case_sensitive = true);
+
+
+
+ The dot was chosen because it's only one character, (as opposed to four
+ characters, with the pub 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.
+
+
+My Proposal
+
+
+ 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 argument were last seriously
+ proposed, so I think now is a good time for a new proposal.
+
+
+Summary
+
+
+ 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.
+
+
+Motivation
+
+Improve safety and readability
+
+
+ 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: hard_link.
+
+
+
+pub fn hard_link<P: AsRef<Path>, Q: AsRef<Path>>(
+ original: P,
+ link: Q,
+) -> Result<()>
+
+
+
+ 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.
+
+
+
+ With named parameters, it is easy to see what is happening at the call-site.
+ This also makes the resulting code more readable.
+
+
+
+hard_link(original: "a.txt", link: "b.txt")?;
+
+
+The effect is more pronounced for functions which have more parameters.
+
+
+// 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);
+
+
+
+ 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.
+
+
+Boilerplate reduction
+
+
+ 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 None into every
+ parameter comes with readability issues.
+
+
+
+repository.checkout_index(None, None)?;
+
+
+
+ Passing a struct into the parameters does give us names, but still requires
+ us to specify None for the optional values. Sometimes this
+ boilerplate can be reduced by adding ..Default::default() to the
+ end of the constructor, Default cannot be implemented if any of
+ the fields are not optional. Implementing Default 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
+ RFC 3681, but it
+ still requires a new struct to be defined specifically for one function.
+
+
+
+let instance = Instance::new(InstanceDescriptor {
+ backends: Backends::default(),
+ flags: InstanceFlags::default(),
+ memory_budget_thresholds: MemoryBudgetThresholds::default(),
+ backend_options: BackendOptions::default(),
+ display: None,
+});
+
+
+
+ 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
+ bon 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 Command struct uses 60 SLOCs (and
+ many more lines of documentation) to allow us to do this:
+
+
+
+Command::new("printenv")
+ .stdin(Stdio::null())
+ .stdout(Stdio::inherit())
+ .env_clear()
+ .envs(&filtered_env)
+ .spawn()
+ .expect("printenv failed to start");
+
+
+If we had named arguments, the following function would be easy to define:
+
+
+spawn(
+ command: "printenv",
+ stdin: Stdio::null(),
+ stdout: Stdio::inherit(),
+ env_clear: true,
+ envs: &filtered_env
+).expect("printenv failed to start");
+
+ Bon also provides utilities for emulating named arguments in functions, but
+ calling such a function involves some boilerplate as well.
+
+
+
+// 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");
+
+
+
+ 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.
+
+
+Extensibility without breaking changes
+
+
+ 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.
+
+
+
+ This leads to some APIs having many functions which do similar things, but
+ with different parameters. For example, HashMap from the standard
+ library has the following factories.
+
+
+
+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;
+}
+
+
+
+ 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.
+
+
+
+pub fn new(
+ pub capacity: usize = 0,
+ pub alloc: A = Global,
+ pub hasher: S = RandomState,
+);
+
+
+
+ If we wanted to some day add a fill 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 HashMap.
+
+
+Guide-level explanation
+
+
+ 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 pub keyword before its name.
+
+
+
+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');
+}
+
+
+
+ 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.
+
+
+
+print_labeled_measurement(value: 5, unit_label: 'h');
+
+
+
+ When parameters are named, they may also be re-ordered, so you don't need to
+ remember the position of each one.
+
+
+
+print_labeled_measurement(unit_label: 'h', value: 5);
+
+
+
+ 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.
+
+
+
+// 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);
+}
+
+
+
+ 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.
+
+
+
+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)
+}
+
+
+
+ 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.
+
+
+
+fn print_coordinates(pub &(x, y): &(i32, i32)) {
+ // ...
+}
+
+
+
+ However, a name can be given to this parameter by using an @
+ binding.
+
+
+
+fn print_coordinates(pub point @ &(x, y): &(i32, i32)) {
+ // ...
+}
+
+
+
+ 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.
+
+
+
+trait Trait {
+ fn f(pub a: i32, pub b: i32);
+}
+
+impl Trait for () {
+ // This is okay, because the parameter names match
+ // unused parameter warnings can be ignored using @
+ 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) {}
+}
+
+
+
+ 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 Fn* family of traits.
+
+
+
+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
+
+
+Reference-level explanation
+
+Grammar
+
+
+ The only needed change to support public parameters in function declarations
+ is to function parameters. First, we add the pub keyword, which
+ must be followed by an identifier pattern. Such a pattern may also include a
+ default value.
+
+
+
+FunctionParamPattern = PatternNoTopAlt ":" ( Type | "..." )
+ | "pub" IdentifierPattern ":" ( Type | "..." ) ( "=" Expr )?
+
+
+
+ For function calls, the syntax is slightly more complicated to parse, but only
+ affects CallParams.
+
+
+
+CallParam = Expression ( ":" Expression )?
+CallParams = CallParam ( "," CallParam )* ","?
+
+
+
+ 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.
+
+
+Static semantics
+
+Function parameters
+
+
+ Given a FunctionParamPattern where the default is specified, i.e.:
+
+
+
+FunctionParamPattern = "pub" pat:IdentifierPattern ":" ( ty:Type | "..." ) ( "=" expr:Expr )
+
+
+similar rules to the rules for default field values apply. Namely,
+
+ -
+ If the function is
const, then the expression, expr
+ must be a constant expression.
+
+ - The expression
expr must coerce to the type ty
+ -
+ Generic parameters of the current items are accessible.
+
+fn foo<const A: usize>(bar: usize = A) {}
+
+
+ -
+ Default const expressions are not evaluated at definition time, only during
+ instantiation. This means that the following will not fail to compile:
+
+fn foo(a: usize = panic!(), b: usize = 42) {}
+
+foo(a: 0);
+
+
+ -
+ The function's generic parameters are properly propagated, meaning the following is possible:
+
+fn foo<T>(bar: Vec<T> = Vec::new()) {}
+
+foo::<T>();
+
+
+ -
+ When lints check attributes such as
#[allow(lint_name)] are
+ placed on a FunctionParam, it also applies to the expression,
+ expr.
+
+ -
+ Unused parameter names will not emit a warning if they are bound to a
+ different pattern using
@. The bound pattern may still emit a
+ warning
+
+
+
+
+ For public parameters in general, all public parameters must be strictly after
+ all non-public parameters.
+
+
+Function calls
+
+
+ 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.
+
+
+In the case of the following production of a named argument:
+
+CallParam = name:Expression ":" value:Expression
+
+the following rules apply
+
+ - The expression,
name must be a plain identifier.
+ -
+ The identifier,
name must be the name of a public parameter of
+ the function.
+
+ -
+ The expression,
value must coerce the the type of the
+ aforementioned public parameter.
+
+
+
+Trait declarations
+
+
+ For trait declarations all the same rules apply from function declarations. In
+ addition:
+
+
+ - Trait methods cannot have default parameter values.
+
+
+Trait implementations
+
+
+ The semantics of trait implementations vary slightly from the semantics for
+ trait declarations.
+
+
+ -
+ Any parameter which was declared public on the trait declaration must also
+ be public in the trait implementation.
+
+ - Public parameters must have the same name that their declaration has.
+
+
+Drawbacks
+
+Added complexity
+
+
+ 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.
+
+
+
+ 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.
+
+
+Named arguments aren't mandatory
+
+
+ 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.
+
+
+
+ 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.
+
+
+
+ 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.
+
+
+
+// before refactor
+coordinates(y: 45.6, x: 6784.0);
+
+// after refactor: incorrect order
+coordinates(y, x);
+
+
+Remedy: Add a clippy lint
+
+
+ A clippy lint can be used to enforce the use of named arguments when possible.
+
+
+Remedy: Require named arguments in a future edition
+
+
+ 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.
+
+
+Limiting type ascription
+
+
+ Many older proposals for named arguments noted that the syntax chosen here:
+ name: value 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.
+
+
+Rationale and alternatives
+
+Why make named arguments opt-in?
+
+
+ Although many languages, such as Kotlin and C#, allow any argument to be
+ named, this is not practical for Rust. Changing an argument 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.
+
+
+The pub keyword
+
+
+ Using the pub 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.
+
+
+
+ Other modifiers have been proposed. One could imagine adding an ext
+ 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
+ @ may be more acceptable. Another proposed idea is to copy Swift
+ syntax and use two identifiers.
+
+
+
+fn foo(bar bar: &str) {}
+
+
+
+ Re-typing the same parameter name twice is verbose and usually unnecessary.
+ It's also unnecessary since syntax exists for rebinding a variable already.
+
+
+
+ Dart wraps named arguments in curly braces. This has the benefit of not
+ requiring the pub keyword to be specified multiple times. However,
+ in the context of Rust, this looks like a destructuring pattern, which isn't
+ semantically correct.
+
+
+
+// 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))
+
+
+
+ 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.
+
+
+
+fn foo (a: u32) { b: u32 } {}
+foo(1) { b: 2 };
+
+
+Colon vs equal sign
+
+
+ Some have proposed using an equal sign (=) instead of a colon
+ (:). This would make the syntax more closely resemble assignment,
+ which is semantically true. It also would not possibly conflict with type
+ ascription.
+
+
+
+ The colon syntax was chosen for this RFC to be more consistent with struct
+ initializers.
+
+
+Argument re-ordering
+
+
+ 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.
+
+
+
+ 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.
+
+
+No default values in traits
+
+
+ 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.
+
+
+On const contexts
+
+
+ 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.
+
+
+
+ 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.
+
+
+Named arguments aren't mandatory
+
+See the section above in "Drawbacks".
+
+Alternative: Do nothing
+
+
+ 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.
+
+
+Alternative: Structural Records
+
+
+ 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.
+
+
+
+fn foo({ a, b, c }: { a: f64, b: f64, c: f64 }) {}
+
+
+There are several criticisms I have of this pattern:
+
+ -
+ It cannot be backported to older functions without causing a breaking change,
+ so the standard library could not use it.
+
+ -
+ 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.
+
+ -
+ Structural records have had a proposed RFC in the past, but was rejected,
+ mainly due to the complexity of implementing it.
+
+
+
+
+ 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.
+
+
+Alternative: Struct name inference
+
+
+ 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.
+
+
+
+foo(_ { a: 1, b: "2", c: [] });
+
+
+
+ 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.
+
+
+Alternative: Implementing Fn traits
+
+
+ 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 Fn 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.
+
+
+Prior art
+
+
+ Several previous attempts at named arguments, both in Rust and in other
+ languages, have been described throughout this RFC.
+
+
+
+ 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.
+
+
+ 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.
+
+
+ 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.
+
+
+ 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.
+
+
+
+ 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.
+
+
+
+void enableFlags({bool? bold, bool? hidden})
+enableFlags(bold: true)
+
+
+
+ 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.
+
+
+Unresolved questions
+
+
+ - Should we require named arguments to be given in position order?
+ - Is
pub the best keyword, or should a different keyword be used?
+ - Should default values be required to always be const?
+
+
+Future possibilities
+
+Default parameter values for trait methods
+
+
+ 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.
+
+
+
+ One solution would be to only allow default values to be used in non-dyn
+ contexts.
+
+
+
+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
+}
+
+
+Extending traits
+
+
+ Theoretically, there's nothing preventing the following, as long as a default
+ parameter value is provided:
+
+
+
+impl<T> Default for Vec<T> {
+ fn default(capacity: usize = 0) -> Self {
+ Self::with_capacity(capacity)
+ }
+}
+
+
+
+ Although, in addition to making the behavior of the default
+ function inconsistent, this is even harder to resolve in a dyn
+ context. The likely remedy would be to forbid Vec from being
+ converted into a dyn Default, which would make introducing the
+ named parameter a breaking change.
+
+
+Requiring named arguments in a future edition
+
+
+ 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.
+
+
+
+ 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.
+
+
+More flexible re-ordering
+
+
+ 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.
+
+
+
+ C#'s main rule is that out-of-order named arguments cannot be followed by
+ positional arguments.
+
+
+
+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");
+
+
+Defaults Affect Inference
+
+
+ In 2022, Gankra proposed using default parameter values as part of inference
+ for generic type parameters. We could imagine something like this.
+
+
+
+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();
+
+
+
+ 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 S is
+ RandomState. Although, that's already true with the current set
+ of HashMap factories, unless you use default which
+ doesn't have any parameters at all.
+
+
+Generic const
+
+
+ In the case of Vec, adding an optional capacity
+ parameter is not possible with this iteration of the RFC, because
+ Vec::new is a const 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:
+
+
+
+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(),
+ }
+ }
+}
+
+
+
+ If no argument value is provided, then the function would be
+ const. But when a usize is provided, then it would
+ no longer be const.
+
+
+
+
--
cgit v1.3.1