This is my first attempt at an RFC, any feedback is welcome
Keyword arguments have been proposed multiple times in multiple forms in the past and had been postponed for after the 1.0 release. 1.0 has been released and things have settled a bit, so I thought it would be a good time to jump-start the discussions more actively again.
Let me know if some parts need more clarification / details.
Relevant discussions
- Feature Name: named_arguments
- Start Date: 2016-08-07
- RFC PR: (leave this empty)
- Rust Issue: (leave this empty)
Summary
This RFC adds named arguments to Rust. It focuses only on named arguments and not on default arguments or variable-arity which are more complex to implement and delicate to get right.
Motivation
Rust strives to be maintainable for large code bases. named arguments would allow some function calls to be more explicit and thus easier to read and write.
Let’s consider the following fictive examples:
http.get("https://www.rust-lang.org/en-US/", None);
Window::new("Title", 20, 50, 500, 250);
free_fall(100.0, 0.0, 9.81);
Those are all functions / methods that I could see written in Rust. When calling those functions you have to remember the order in which the arguments appear. And at first sight, you can not really tell what the arguments stand for.
Now let’s consider calling the same functions with named arguments:
http.get("https://www.rust-lang.org/en-US/", timeout: None);
Window::new(title: "Title", x: 20, y: 50, width: 500, height: 250);
free_fall(z0: 100.0, v0: 0.0, g: 9.81);
It is now very clear what the arguments stand for, which makes maintaining such code a little easier.
Detailed design
The main goals of this RFC are:
- Make it backwards compatible
- Make it future proof
- Give API authors the ability to choose if the name of the arguments are part of the API or not
API authors are in control
Let’s start with the last point. Many proposals to add named arguments to Rust assumed that all arguments would become optional named arguments. However, this would make all the argument names part of the public API and renaming them would become a breaking change.
In this proposal I wish to give the ability to the API authors to choose if they want to commit to the argument names by making them part of the public API.
I propose to do this with the following syntax:
// Normal function with positional arguments
pub fn positional(a: i32, b: i32) -> i32;
// Function with named arguments
pub fn named(pub a: i32, pub b: i32) -> i32;
In the second function, we have explicitly marked the arguments as public
.
Why pub
?
-
pub
is already a keyword. - Using it here would coincide with it’s meaning: “making something public / part of the API”.
- The use of pub would make it very clear that this argument is facing the outside world, making it part of the API and everything that that implies.
API authors would be able to start with positional arguments, like we have now. Until they find they have found a good enough API design. At that point they can just mark all or some arguments as public
without any breaking change.
Backwards compatible
This proposal is 100% backwards compatible. There is no change in behavior unless arguments are explicitly marked as public, in which case users will be able to call the function or method with either positional arguments or named arguments.
Future proof
Named arguments are often tightly coupled with default arguments because they complement each other very well. In this RFC I intentionally restrict the scope to named arguments because default arguments often spark lengthy debates over the “good” patterns to use in API design. This means however that we have to make sure in this RFC that we are not closing any doors for the future.
As far as I can tell, this proposal is a relatively isolated syntactic sugar. Default arguments could easily be added in the future:
// Function with default named arguments
pub fn default_arguments(pub a: i32 = 0, pub b: i32 = 2) -> i32;
(Syntax to be determined by a future RFC)
Other clarifications
- Once you make an argument public, the user can use both forms (positional or named). This allows API authors to “promote” positional arguments to named arguments without any breaking change.
- A positional argument can not appear after a named argument both in the declaration as well as at the call site.
- Order of named arguments does not matter, they are resolved by their name and not by their order / position.
Drawbacks
Not sure if any?
Alternatives
Do nothing
Currently you can improve explicitness by wrapping the arguments in struct
s or use the builder pattern.
struct Window { /* Some fields */ }
struct Position { x: i32, y: i32 }
struct Dimension { width: u32, height: u32 }
impl Window {
pub fn new(title: &str, pos: Position, dim: Dimension) { /* Some code */ }
}
fn main() {
Window::new(
title: "Title",
Position{ x: 20, y: 50 },
Dimension{ width: 500, height: 250 }
);
}
In this example, it works, and some could argue it is even the better choice. But what about
free_fall(z0: 100.0, v0: 0.0, g: 9.81);
There is nothing to group. You could make a FreeFallArgs
struct but this would require more boilerplate for a more verbose syntax.
Struct sugar
Add syntactic sugar to make struct
s more ergonomic to use in functions. For example:
struct FooParams { a: u32, b: u32, c: u32 }
foo({a: 1, b: 2, c: 3});
The braces could even be dropped, it would be less obvious that it is a struct
though.
Grouping arguments into struct
s where it makes sense and judicious use of the builder pattern are good design patterns. But I don’t think they are the most ergonomic solution in all situations.
Named-only arguments
Arguments that can only be called with their named form and not by their position, could prove useful to enforce better APIs. For example, API authors could require booleans to be named-only arguments. This would force the user to be explicit at the call-site and makes bad unmaintainable code just a little bit harder to write.
The downside is that you loose the ability to “promote” positional arguments to named arguments without braking change.
See PEP-3102 for more background.
Parameters trait
Add a special trait, for example Params: Default
and have syntactic sugar for invoking functions that have their last argument implementing Params
.
Also add syntactic sugar for automatically defining an anonymous params struct.
For example:
#[derive(Params, Default)]
struct ExplicitParams {
herp: u8,
derp: i8,
}
// Use this form when you have many parameters
fn explicit(durr: &str, named: ExplicitParams) { ... }
// Use this from when you only have few parameters
// The params struct is automatically defined for you which takes away a lot of the verbosity
fn implicit(foo: &str = "xy", bar = 1u8) { ... }
fn main() {
explicit("unnamed", derp = 2);
// desugared: explicit("unnamed", ExplicitParams { derp: 2, ..Default::default() })
implicit(bar = 2);
// desugared: implicit(AnonymousStruct { foo: "xy", bar: 2 })
}
This would also take care of default arguments at the same time!
See: https://github.com/rust-lang/rfcs/issues/323#issuecomment-165184703 for the original proposal.
Unresolved questions
No unresolved questions currently.