Pre-RFC: Named arguments

This is my first attempt at an RFC, any feedback is welcome :). I have read the previous topics and (except if I missed one) ensured there is not another open on the same subject right now.

The most up-to-date version can be found at GitHub - poliorcetics/named-arguments-rfc: A RFC to add named arguments to the Rust programming language. This is a work in progress., I will try to remember to update here each time I make a change.

Named arguments have been proposed before, several times, in several forms. This document will list below some links found on this forum about them or in past RFCs. It will also review what exists in other programming languages before presenting this RFC's solution.

But first, some goals for this RFC:

  • The solution must be backward compatible so that it can be backported to Rust Edition 2015, 2018 and 2021. Even if a backport is not wanted, having it has a goal means this RFC should not conflict with features existing in one edition but not in another.
  • The solution should improve readability/usability without requiring new keywords or much in the way of typing (so that programmers can use them without having to write a line and a half just for some non-critical feature).
  • Clearly present other options and alternatives (maybe one of them is better and someone will see it before a wrong choice is made).
  • Default parameters and variadic parameters are out of scope. They are orthogonal to named arguments and can be considered with or without named arguments.
  • Overloading is in scope. The named arguments solution proposed will affect function resolution.

Summary

This RFC introduces named arguments for Rust. Named arguments are the ability to call a function , method or closure while precising the caller-facing name of the arguments, greatly improving clarity in many situations. Of course functions where the argument is already clear do not have to use them: sin(x: x) would be ridiculous and redundant. Named arguments as proposed here are not source-breaking: existing functions and methods will continue to work without any change. Named arguments as proposed here are opt-in. Changing the name of f(a: usize) to f(b: usize) will not be a breaking change, just like it is not today: once again, the goal is not to force them on developers but to provide another option that has seen huge success in other languages in a way that fit with Rust.

Named arguments also introduce a limited form a function overloading that is easy to check for both a human and the compiler and can be applied to profit plainly from heavily used function names like new.

An example in Python for clarity:

def open_port(port: int, max_connexions: int, timeout: int):
    print(f"Port: {port}")
    print(f"Max conn: {max_connexions}")
    print(f"Timeout: {timeout}")

# Calling the function with names for the arguments.
open_port(port=12345, max_connexions=10, timeout=60)

Motivation

The main point of this section is that named arguments make for harder to misuse interfaces through clarity and simplicity of both declaration and usage.

  • Named arguments increase readability.
// Which is the insertion index and which is the element ?
my_vec.insert(2, 3)

// Clearer without asking for much
my_vec.insert(2, at: 3)

// Maybe too much named arguments ?
my_vec.insert(elem: 2, at: 3)
  • Named arguments are self documenting.

In the example code above it is not easy for a developer to remember which argument does what simply by looking at the method call, without having to write a little toy example or look at the documentation for the type (or the trait). Most of the time Rust helps by asking for different types in its parameters, but that fails when the type is the same. Autocompletion can help by providing the names and filling them in advance, meaning no more typing for most people, just clearer code. Code is also read more than it is written, the clearer it is, the less mistakes slip through reviews.

  • Named arguments are simple to create compared to the other options.

In all languages that have them, named arguments are simple to create: they do not require a new type and they do not require a builder pattern (and so another type). This does not means that builder patterns or new types are useless: I argue that the use cases are simply not the same. Named arguments should be used to clarify function calls, not write functions and methods with 13 parameters, 7 of which are named: a builder would be more useful in this situation.

  • Named arguments are simple to use compared to the other options.

Calling a builder for the my_vec.insert call above is clearly over-engineering and creating a type for such a simple operation is overkill too. Named arguments are made to fill this spot where the other solutions are too big for what's intended but clarity is lost without something more than positional arguments, especially when types do no conflict.

  • Named arguments can be combined with other features to increase readability even more.

It is possible to combine builders/new types with named arguments without problems:

// Hypothetical Rust syntax for named arguments that does not conflict with
// a ConnectionOptions type.
pool.connect("https://a.b.c/endpoint/", with: ConnectionOptions {
    timeout: 200,
    account: "name",
    ..Default::default()
});

// The same with a builder. Note that the builder takes advantage of the
// hypothetical named arguments syntax too.
pool.connect("https://a.b.c/endpoint/", with: ConnectionsOptionsBuilder::new()
    .timeout(seconds: 200)
    .account("name")
    .build()
);
  • Reality.

Taken from a previous RFC

Instead of looking at how code could be written in carefully crafted APIs, we should look at how code is being written in reality. Programmers don't always have time to rack their brains over how to create the most beautiful API. They want to get things done.

Named arguments allow iterating quickly without sacrificing readability, because they are dead simple. There's no need to create new types or make up long function names.

As an example: the (amazing) cargo tool would have a use for named arguments:

// Code in cargo

compile_opts.filter = ops::CompileFilter::new(
    LibRule::Default, // compile the library, so the unit tests can be run filtered
    FilterRule::All, // compile the binaries, so the unit tests in binaries can be run filtered
    FilterRule::All, // compile the tests, so the integration tests can be run filtered
    FilterRule::none(), // specify --examples to unit test binaries filtered
    FilterRule::none(), // specify --benches to unit test benchmarks filtered
); // also, specify --doc to run doc tests filtered

VS

// Possible code with named arguments

compile_opts.filter = ops::CompileFilter::new(
    library: LibRule::Default,
    binaries: FilterRule::All,
    tests: FilterRule::All,
    examples: FilterRule::none(),
    benches: FilterRule::none(),
); // also, specify --doc to run doc tests filtered

Note that in the example above, Rust type system cannot help: the last four arguments are all of the same type.

  • Improve soundness and safety.

The documentation for Vec::reserve_exact shows clearly the parameter is for additional capacity. But is it always clear in code ?

let mut vec = vec![1];
// Is this `additional` or `total` capacity ? The name of the method is quite
// unclear here.
vec.reserve_exact(10);

It is the same for f64::atan2: is the parameter x or y when calling orig.atan2(angle) ? Here rust-analyzer cannot even help since the internal parameter name is other. The only way to know is documentation.

An argument against named argument is that hints like those provided by Rust-Analyzer are here for those cases. This is true, but they are not always available. They can be disabled, reviewing a PR through a web interface does not have them, reading code on GitHub will not show them, maybe your coworker does not like them, there are many reasons for them not to appear. Named arguments are part of the code, they always appear when intended to. Just like types, they help by adding another layer of clarity to code, which helps with soundness and safety, and just like types can be inferred when writing let a = b + c, named arguments as proposed here are not mandatory for each and every function: forcing sin(x: x) is not improving anything.

  • Improve coherence in the language.

Named arguments already exists for structs today: Latitude { x: 42.1, y: 84.2 }, having named arguments for functions can be seen as an extension of that capability.

The previous paragraph opens an argument against: Wrapper(x) does not have named arguments and it is quite clear. I would argue this is false: the argument name is the name of the type itself. Wrapper types are here to increase clarity and provide additional guarantees through the type system, and they do so by being explicit (NonZeroUsize, NonNull and friends are wrapper types that make their usage clear through their name for example).

  • Allow for a form of function overloading that is clearly visible.

This would allow reusing short function names while adapting them to context or similar capabilities, as is already possible for types through the use of generics. See the example in the guide-level explanation for details.

Guide-level explanation

Explain the proposal as if it was already included in the language and you were teaching it to another Rust programmer. That generally means:

  • Introducing new named concepts.
  • Explaining the feature largely in terms of examples.
  • Explaining how Rust programmers should think about the feature, and how it should impact the way they use Rust. It should explain the impact as concretely as possible.
  • If applicable, provide sample error messages, deprecation warnings, or migration guidance.
  • If applicable, describe the differences between teaching this to existing Rust programmers and new Rust programmers.

For implementation-oriented RFCs (e.g. for compiler internals), this section should focus on how compiler contributors should think about the change, and give examples of its concrete impact. For policy RFCs, this section should provide an example-driven introduction to the policy, and explain its impact in concrete terms.


Named arguments are the ability to call a function, method or closure while precising the caller-facing name of the arguments, greatly improving clarity in many situations. Of course functions where the arguments are already clear do not have to use them: sin(x: x) would be ridiculous and redundant.

This section will explain how to declare and use named arguments as a teacher may explain mathematics: it will present the concepts while abstracting away much of the reasoning, which is detailed more thoroughly in other sections (see [Reference-level explanation][reference-level-explanation]). It is divided into three parts: declaring, calling and other details.

Declaring a function using named arguments

There are two way to mark an argument as named when declaring a function (or method):

  • With the pub keyword (only when the binding is not a pattern).
  • With another identifier (that cannot be a pattern itself).

The following example presents both methods in their simplest form. Further examples will explain how edge cases are handled.

pub struct Database;
pub struct RegistrationError;

pub fn register(
    pub name: String,
    pub surname: String,
    to db: Database
) -> Result<(), RegistrationError> {
    /* ... */
}

The pub name: String part marks the binding name as public: it must be provided when calling the function and can be used inside the function too.

The to db: Database part marks the binding to as public and the binding db as internal: to must be used when calling the function and cannot be used inside its definition. db is in the opposite situation: it cannot be used outside the function's definition.

Using fn register(pub to db: Database) is an error.

Using fn register(pub(in path) name: String) is also an error: named arguments always have the exact same visibility as the function they belong to. They must be used anytime the function is called so it is not possible to limit them to an arbitrary scope that is different from the function's.

Declaring closures with named arguments

Just like regular function, it is possible to declare closures with named arguments:

pub struct Point { x: f32, y: f32 }

impl Point {
    // Using `Fn` form
    pub fn strange_operation(&self, f: impl Fn(add: f32, mul: f32) -> (f32, f32)) -> (f32, f32) {
    //                                         ^^^       ^^^ named arguments declared here
        f(add: self.x, mul: self.y)
    //    ^^^          ^^^ and used here
    }
}

// Using closure form
let closure = |pub add, other arg| { (add + 42.0, arg * 42.0) };

Just like functions, add and arg must be used inside the function when declared, other is not available.

When using self

The previous example about register works but it's contrived and not very idiomatic. It could instead be rewritten like this:

pub struct Database;
pub struct RegistrationError;

impl Database {
    pub fn register(&self, pub name: String, surname: String) -> Result<(), RegistrationError> { /* ... */ }
}

This example raises a question: what about the &self part ? Can it be named ? Can it be marked with the pub keyword ?

No, it cannot. Named arguments are here to increase clarity. Call to methods are already as clear as possible: either done through my_db.register(name: "Alexis".into(), surname: "Poliorcetics".into()), with the . clearly marking the called function and the caller or through the qualified syntax in which the type (or trait) cannot be omitted: Database::register(my_db, name: "Alexis".into(), surname: "Poliorcetics".into()).

When using mut or ref

mut cannot be an identifier for an argument but it can be used by a function to avoid a let mut arg = arg; inside. This capability does not go away with named arguments.

  • When using pub; mut is placed after it to follow the current syntax of Rust where the visibility always comes first: fn register(pub mut name: String).
  • When using an identifier, mut comes first: fn new_db(mut named name: String) -> Database.

The exact same rules apply for ref.

If both ref and mut are present, they use the same order as today: ref mut, and with pub: pub ref mut.

When using a pattern

Irrefutable patterns can be used in functions arguments today, and just like self, they raise some questions.

  • pub cannot be used here since there is no identifier for it to expose.
  • The identifier cannot be a pattern. Its only use is as a public facing name, it does not de-structure anything nor can be used as a binding inside the function.
  • The identifier is placed before the pattern as shown in the example below:
struct Point { x: f32, y: f32 }

impl Point {
    fn opposite(&self, centered_on Self { x, y }: Self) -> Self {
        Self {
            x: 2.0 * x - self.x,
            y: 2.0 * y - self.y,
        }
    }
}

Combining patterns and mut/ref

This has the same behavior as current Rust: it is impossible to mark all the bindings in a pattern as mutable at once:

// ERROR
impl Point {
    fn opposite(&self, mut centered_on Self { x, y }: Self) -> Self {
    //                 ^^^ does not compile
        Self {
            x: 2.0 * x - self.x,
            y: 2.0 * y - self.y,
        }
    }
}

// OK
impl Point {
    fn opposite(&self, centered_on Self { mut x, y }: Self) -> Self {
        Self {
            x: 2.0 * x - self.x,
            y: 2.0 * y - self.y,
        }
    }
}

Calling a function with named arguments

This has been hinted at in the previous subsection, so here is the syntax, using the same examples as before:

// Free function
register(name: "Alexis".into(), surname: "Poliorcetics".into(), to: my_db);

// '.' call
my_db.register(name: "Alexis".into(), surname: "Poliorcetics".into());

// Qualified call
Database::register(my_db, name: "Alexis".into(), surname: "Poliorcetics".into());

Functions and methods are called as usual, the parameters can be any expression that eventually resolves to the correct type for the argument, but there is the identifier and a : before said expression.

You cannot omit named arguments, even when the passed expression is exactly the same as the identifier: my_db.register(name: name) cannot be shortened to my_db.register(name).

Calling a function with named arguments indirectly

All examples until now have always called the function (or closure) directly, but Rust also allows us to pass functions and closures as arguments. Below is how named arguments behave in such a case:

pub struct Point { x: f32, y: f32 }

impl Point {
    pub fn strange_operation(
        &self,
        f: impl Fn(add: f32, mul: f32) -> (f32, f32)
    ) -> (f32, f32) {
        f(add: self.x, mul: self.y)
    }
}

let closure = |pub add, other arg| { (add + 42.0, arg * 42.0) };

fn twos(x: f32, y: f32) -> (f32, f32) {
    (x + 2.0, y * 2.0)
}

fn twos(pub x: f32, y: f32) -> (f32, f32) {
    (y + 2.0, x * 2.0) // inverted x & y
}

// Long versions, always valid, exact match for function signature
some_point.strange_operation(|pub add, pub mul| closure(add: add, other: mul))
some_point.strange_operation(|pub add, pub mul| twos(add, mul))
some_point.strange_operation(|pub add, pub mul| twos(x: add, mul))

// No need for exact match though since each closure is unique here
some_point.strange_operation(|add, mul| closure(add: add, other: mul))
some_point.strange_operation(|add, mul| twos(add, mul))
some_point.strange_operation(|add, mul| twos(x: add, mul))

// Disambiguation version
// Those are NOT method calls and the ending ':' is mandatory with this syntax,
// just as '_' is for anonymous arguments
some_point.strange_operation(twos(_:_:))
some_point.strange_operation(twos(x:_:))
some_point.strange_operation(closure(add:other:))

Note how the names declared in the Point::strange_operation's f closure are not mandatory at the call site: some_point.strange_operation(twos(_:_:)) does not expose the names expected but it still works: this is a feature, which 'casts' argument names when passing a function as closure. It is here to help with brevity and clarity: while we could require the long form all the time, it would be heavy and does not add much value since the two versions after are still unambiguous in terms of the passed function.

Disallowed calls

Note that if overloading brings two versions with a different number of parameters, it is still necessary to be explicit about which function is passed, to ensure clarity for readers:

fn twos(pub x: f32, pub y: f32) -> (f32, f32) {
    (x + 2.0, y * 2.0)
}

fn twos(pub x: f32) -> (f32, f32) {
    (x + 2.0, x * 2.0)
}

some_point.strange_operation(twos(x:y:)) // OK
some_point.strange_operation(twos) // ERROR, even if unambiguous from the parameter count POV,
                                   // syntax reserved for a function with no arguments at all

See [Overloading resolution][overloading-resolution] for details on this behavior.

Other points

Using named arguments with traits

Named arguments are fully usable in traits and types implementing those must respect the public facing name of the argument, the private one can be modified in implementations:

trait Connection {
    fn connect(&mut self, pub port: usize);
}

struct Dummy;

impl Connection for Dummy {
    fn connect(&mut self, port _: usize) {
    //                    ^^^^   Public name is the same
    //                         ^ Name has been changed internally
    }
}

fn create_conn<T: Connection>(t: &mut T) {
    t.connect(port: 443)
    //        ^^^^ Public name declared by trait is used in call.
}

Overloading a function's name with named arguments

Named arguments introduce a limited form a function overloading that is easy to check for both a human and the compiler and can be applied to profit fully from heavily used function names like new. This overloading is based on both the function's name and the public names of all the named arguments, ensuring two overloaded functions side by side cannot be mistaken for one another: the information is always present, even when reading code without tooling to show type and name hints.

In the example below, calling my_result.ok() and my_result.ok(or: default_value) would call two different functions. The third function would be banned because it uses the same public name as the second one.

impl<T, E> Result<T, E> {
    pub fn ok(self) -> Option<T> {
        match self {
            Ok(t) => Some(t),
            Err(e) => None,
        }
    }

    pub fn ok(self, or fallback: T) -> T {
        match self {
            Ok(t) => t,
            Err(_) => fallback,
        }
    }

    // ERROR
    pub fn ok<U>(self, or replacement: U) -> T where U: Into<T> {
    //     ^^----------^^----------------
    //     A function using this name and this named argument already exists.
        match self {
            Ok(t) => t,
            Err(_) => replacement.into(),
        }
    }
}

You can think of this form of overloading as the function-level equivalent of Result<T, E>, where the simple name of the type Result is not enough for disambiguation: you have to provide the parameters and so Result<Option<()>, ()> and Result<(), ()> are different types overloading the same root name. This is an integral part of the Rust type system and is checked at compile time, just like named arguments.

Mixing named and unnamed arguments

Yes it is possible, without any restrictions (beside the one on self in methods):

// Calling with an unnamed and a named argument.
my_vec.insert(2, at: 3)

// Declaring a function which mixes named and unnamed arguments in some arbitrary order.
fn mix_and_match(pub named: usize, unnamed: usize, public hidden: usize) { /* ... */ }

Reordering named arguments when calling

No it is not possible. Just like unnamed arguments and generics, named arguments are also position-based and cannot be reordered when calling: register(name:surname:) cannot be called as register(surname:name:).

Reordering them at the definition site is an API break, just like reordering unnamed arguments or generics is an API break already.

Documenting named arguments

Talking about functions using named argument uses register(name:surname:), not just register(). This allows differentiating overloads clearly and make it easier to remember named arguments are used for the function. Cases where one argument is public and the other is not are written as register(_:surname:). Of course, using the shorthand register() is fine when clear in context, just like we use Result to talk about Result<T, E>, though this form is intended to be only used when there are no public arguments, to ensure maximal clarity for readers.

rustdoc shows the internal name of arguments already when generating documentation for Rust code. While leaky, this is very useful to understand some parameters and have names to refer to in textual documentation, like for f32::mul_add, and removing it to instead show only named arguments would be very detrimental to the user experience.

Instead rustdoc would now behave as such:

  • Insert the keyword pub before arguments that are public and declared with pub: fn register(pub name: String).
  • Insert both the public and private name for arguments that use an identifier: fn opposite(&self, centered_on _: Self) -> Self. This is taken from how Swift does it.
  • Keep the behavior of showing _ when a pattern was used as the argument (like above).
  • Keep hiding mut and ref like currently done.
  • Allow intradoc-links using [link](register(_:surname:)) to differentiate overloads (writing [link](register) would refer to a register function that takes only unnamed arguments, to avoid silently breaking the link if an overload is added).

Reference-level explanation

This is the technical portion of the RFC. Explain the design in sufficient detail that:

  • Its interaction with other features is clear.
  • It is reasonably clear how the feature would be implemented.
  • Corner cases are dissected by example.

The section should return to the examples given in the previous section, and explain more fully how the detailed proposal makes those examples work.


Most points have already been presented in the previous section. This one focuses on those that have not been detailed enough or that are discussed for the first time, to avoid repetition and conflicting information through mistakes in editing.

Two (or more) named arguments with the same public name

There are three cases for this situation:

fn func1(pub name: u32, pub name: u32) { /* ... */ }
fn func2(pub name: u32, name hidden: u32) { /* ... */ }
fn func3(name hidden1: u32, name hidden2: u32) { /* ... */ }
  • func1 is clearly impossible because name would have two different meaning inside the function's definition. Aside from already being an error in today's Rust, it is simply impossible to do for any language that uses names and not position to refer to parameters.

  • func2 and func3 could work in theory: named arguments as proposed in this RFC are position-based and their internal names are different: just like two arguments can have the same type without ambiguity, those functions could be allowed.

But named arguments are a feature designed to improve clarity at the call site and calling register(name: param, name: param2) is just as unclear (and arguably worse) than register(param, param2) so this RFC argues for all those forms to either produce an unconditional error (func1) or to produce an error-by-default lint (func2 and func3).

The error-by-default lint is here because it is theoretically possible for very specific cases to need the same public name twice, but the Swift community has not found such use cases despite their heavy use of named arguments. Python does not allow this situation to occur at all.

Overloading resolution

There is one case that was not mentioned in [Calling a function with named arguments indirectly][calling-a-function-with-named arguments-indirectly]:

pub struct Point { x: f32, y: f32 }

impl Point {
    pub fn strange_operation(&self, f: impl Fn(add: f32, mul: f32) -> (f32, f32)) -> (f32, f32) {
        f(add: self.x, mul: self.y)
    }
}

fn twos(x: f32, y: f32) -> (f32, f32) {
    (x + 2.0, y * 2.0)
}

fn twos(pub x: f32, y: f32) -> (f32, f32) {
    (y + 2.0, x * 2.0) // inverted x & y
}

some_point.strange_operation(twos) // unambiguously refers to `twos(_:_:)`

This special case is necessary to stay compatible with today's Rust and allow named arguments in all editions (which allows us to introduce them in the standard library).

Passing methods and closures with named arguments is not possible in this shorthand form, to ensure the following case always behave correctly:

// Before change

pub struct Point { x: f32, y: f32 }

impl Point {
    pub fn strange_operation(&self, f: impl Fn(add: f32, mul: f32) -> (f32, f32)) -> (f32, f32) {
        f(add: self.x, mul: self.y)
    }
}

fn twos(pub x: f32, y: f32) -> (f32, f32) {
    (y + 2.0, x * 2.0) // inverted x & y
}

some_point.strange_operation(twos) // Previously referred to `twos(x:y:)`,
                                   // now unambiguously and silently refers to `twos(_:_:)`

// Added in a new commit

fn twos(x: f32, y: f32) -> (f32, f32) {
    (x + 2.0, y * 2.0)
}

The compiler would enforce writing some_point.strange_operation(twos(x:y:)) to ensure this silent overload would not happen.

Calling a function with named arguments indirectly, the case of self.

pub struct Point { x: f32, y: f32 }

impl Point {
    pub fn strange_operation(&self, f: impl Fn(add: f32, mul: f32) -> (f32, f32)) -> (f32, f32) {
        f(add: self.x, mul: self.y)
    }
}

The full reference to Point::strange_operation is Point::strange_operation(_:_:), with two unnamed arguments, not one. Writing my_point.strange_operation(_:) is incorrect, just like trying to pass my_point.strange_operation is invalid already.

Interaction with traits

Parameter names are not part of a trait API in today's Rust, justly so: they are helpful guide when looking at documentation but are not part of the public API when calling the method.

This RFC makes the public part of a named argument an API requirement when implementing the trait.

This requirement can be fulfilled in two ways and fail in one, as demonstrated by the following:

trait MyTrait {
    fn func(pub name: u32, name2 hidden: u32);
}

struct ExactSame;
struct OnlyPubliclyTheSame;
struct WrongImpl;

impl MyTrait for ExactSame {
    // Both public and internal names are the same as the trait's declaration.
    fn func(pub name: u32, name2 hidden: u32) { /* ... */ }
}

impl MyTrait for OnlyPubliclyTheSame {
    // The public names are the same but their internal name is different, this is allowed.
    fn func(name hidden1: u32, pub name2: u32) { /* ... */ }
}

impl MyTrait for WrongImpl {
    // The public names are different, this is an API break and is forbidden.
    fn func(pub name0: u32, name1 hidden: u32) { /* ... */ }
}

Traits are one of Rust most powerful feature and this RFC endeavours to integrate well with them, to avoid making them second class citizens.

Interaction with type ascription

Citation from a past RFC:

Using = would be ambiguous, as foo = bar is a valid expression (returning ()). Using : as the RFC suggests would be unambiguous today, but become ambiguous in the future if we ever get type ascription (the ability to say let foo = bar(): int;, which uses a colon to specify the type of an expression), which is something that has been wanted for a long time. An alternative that is unambiguous even with type ascription would be =>.

While this seems to kill any hope of using :, there is missing information here.

First, : fits more nicely with how functions parameters are declared today. = is forbidden for the reason given in the quote above. => is available but reminds of pattern matching when it is absolutely not. A function called as matches(name => param) would be an easy source of confusion.

Second, : as type ascription and : as a delimiter for named arguments do not, in fact, conflict. The first always has the form expr : type, the second expected_identifier : expr.

What's more, there is talk about disallowing type ascription in some places by the lang team.

Interaction with function pointers

In today's Rust, this is perfectly valid, even when using all Clippy warnings:

fn example1(a: u32) -> u32 { a }
fn example2(b: u32) -> u32 { b }

// No public name
let _: fn(u32) -> u32 = example1;
let _: fn(u32) -> u32 = example2;

// Mark a public name as 'a' and the function used 'a' in declaration.
let _: fn(a: u32) -> u32 = example1;

// Mark a public name as 'c' but used 'b' in declaration
let _: fn(c: u32) -> u32 = example2;

This RFC does not modify this behavior. Function pointers are often used in FFI and this behavior is important for it, requiring concordance of named arguments when they do not exist in C would be harmful.

This raises the problem of overload, which can happen in several forms.

The first one is easily fixed by adding a type hint (though that is not the proposed solution):

fn new() -> u32 { 42 }
fn new(using number: u32) -> u32 { number + 42 }

let _ = new;
//  ^ ERROR: cannot determine which `new` function is intended, use a type hint:
//  `: fn() -> u32` or `: fn(u32) -> u32`.

The second one is more complicated:

fn new(adding number: u32) -> u32 { 42 + number }
fn new(removing number: u32) -> u32 { 42 - number }

let _ = new;

Using a type hint in the example above would not work. The solution of asking for the argument name in the type hint cannot work because the syntax let _: fn(c: u32) -> u32 = example2; is already valid today and has no meaning aside from documentation for human. Notably, it is used in FFI with C to document parameters expected by callbacks and changing this would be a potentially huge breaking change.

Instead, the proposed solution adds a new syntax:

fn new(adding number: u32) -> u32 { 42 + number }
fn new(removing number: u32) -> u32 { 42 - number }

let _ = new(adding:);

This would not be a function call (made clear by the : at the end of the parameter list). In case of several arguments, it would be used as new(adding:and:).

This would not raise a problem with type ascription because there would be no type after the :s, especially after the last one and so the compiler would be able to unambiguously decide what is happening.

It would be even easier in the case of a function call: ffi_call(object, new(adding:)) because the compiler would know what to expect as a type for the second parameter of ffi_call here.

About _

It is possible to write fn foo(_: i32) {} today, and it is often used when implementing traits.

This RFC bans fn foo(pub _: i32) {} and fn foo(_ name: i32) {} (and so fn foo(_ _: i32) {}) because it would create an ambiguity with fn foo(_: i32) {} with can be named as foo(_:) and because named arguments are supposed to increase readability: foo(_: 42) is not improving anything about it.

fn foo(name _: i32) {} is of course still available and not banned at all by this RFC: it is using _ as the public name which is banned.

Interaction with #[no_mangle], extern "C" (or anything but the unstable Rust ABI)

Such functions are forbidden from using named arguments if they are overloaded based on them. If they are not, the function can be uniquely identified by just its name even for FFI, which is the point of this attribute. Such functions would still be a warn-by-default lint because having different calling styles for FFI and Rust seems like a Bad Idea(TM) in most cases.

This allows Rust code to call such function using named arguments while C code will not have to use them, and thus makes the following example valid:

#[no_mangle]
extern "C" fn callback(pub return_code: u32) { /* ... */ }

// lib.rs
callback(return_code: 42);

// main.c
callback(42);

This one on the other hand, would not compile:

#[no_mangle]
extern "C" fn callback(pub return_code: u32) { /* ... */ }

#[no_mangle]
extern "C" fn callback(pub data: *const ()) { /* ... */ }

Drawbacks

Why should we not do this?

Overloading

Historically (and not limited to Rust), overloading has been seen as a mixed bag: it allows lots of expressiveness but can quickly become unclear: which type is passed ? Which overload is called ? Is that constructor the move or copy one ?

This has mostly not been a problem for Swift and Python, because their use of overloading is based on something more visible, named arguments, not types invisible without hints. This is the form proposed for Rust and as such, it will not fall prey to the lack of clarity that simple type/number-based overload is subject to.

Rationale and alternatives

  • Why is this design the best in the space of possible designs?
  • What other designs have been considered and what is the rationale for not choosing them?
  • What is the impact of not doing this?

Rationale

There have been several choices made in this RFC that need justification. In no particular order:

  • Using : (see alternatives)
  • Using pub only sometimes
  • Clunkiness of pub
  • Allowing overloading through named arguments
  • Not allowing keywords in the public name (for, in, as especially)

Allowing overloading

The form of overload proposed would notably allow moving the standard library mostly without troubles: Option::ok_or could continue to exist and be deprecated in favor of Option::ok(or:).

The proposed rules for overloading would mean all currently existing Rust code would stay valid since the default resolution for GetClosure::get(my_function) would never call a function with named arguments.

Disallowing keywords

for, in, as are not allowed by this RFC in the position of named arguments. This is for simplicity and clarity: Rust developers and tooling expect them to be keywords, changing this expectation while introducing such a big change to the language would probably be very confusing for some time. They could be allowed later, once named arguments have been here for some time and people have had time to get used to them.

They could also be authorised directly to allow for their use in the standard library for example, if we find several cases where they would be the best fit.

Always use pub

In the Guide Level Explanation, is it said:

Using fn register(pub to db: Database) is an error.

It can be argued that always using pub even when an alternate identifier is provided is both easier for the compiler and clearer for readers of the function. This has not been kept to avoid too much informations in function's declaration. If people feel it would be clearer, that can certainly be changed.

We should not allow both though, it would be redundant and would probably confuse people used to one syntax but not the other.

Never use pub and write the identifier twice

fn register(name name: String) certainly works and is not banned but it is rather redundant and raises a question: did the function writer intend to write pub or use a different name and simply forgot ? Marking such cases as pub makes the original intent clear and reminds the developer that modifying the name is an API break.

pub is not asked for when the two bindings are different because the situation makes it clear already: two identifiers cannot be placed that way next to each other without an operator or a comma anywhere else in normal Rust (it can happen in macros though). Therefore the only possible case is that one name is public and the other is private. Using the first as the public name is then logical: it is in the position of the pub keyword, taking advantage of the similar placement with a similar functionality, which is important for consistency.

Alternatives

Using : instead of =, :=, =>, ...

Especially =

Several macros in the Rust Standard Library have had a form of named arguments for a while:

println!("The answer is {x}{y}", x = 4, y = 2);

The problem is that they use =, not :, unlike this RFC. Despite that, I think it is important to keep : because = remind of an assignment and named arguments are not assigning to anything.

Others

  • := cannot be used backward compatibly because macros could be using it already and changing how it is parsed would break those. I have not done a survey about this so I have no numbers to present. In the spirit of fairness, breaking how some macros are parsed has been done in the past when it was determined very few used the pattern (usually via a crater run).

  • => looks way too much like pattern matching when it is not.

  • -> is used for return types, seems like a bad idea to give a completely different meaning.

Using as

Using as instead of pub can be found as an off-hand idea on Reddit and forums about named arguments and Rust, but it presents some defaults that are not there with pub. But first, here is how it would be used:

fn test(bar as baz: usize) { /* use baz here */ }

// Use bar here
test(bar: 42)

This form is limiting on some points: it is wasteful to have the same public and internal names: data as data, can easily be mistaken for a type cast and it almost entirely precludes introducing context-aware keywords in the future which would make functions like fn EncodedString::new(data: String, as encoding: Encoding) -> EncodedString possible.

Using an alternative sigil like ., @, ... because pub is clunky

Lots of alternative forms have been proposed for named arguments, either as full blown (pre-)RFCs or as quick bike-shedding when discussing those. Most bike-shed options will be ignored since they either ignore the declaration or call point, which is not possible in a serious attempt at named arguments. I will miss others because this section would be longer than the rest of the RFC if I did not.

  • 'name: re-use the lifetime sigil. Lifetimes are already difficult enough (and conflict with labels) without adding a third meaning to '.

  • Using one of @$^#. at both declaration and call point:

foo(@c = 5, @b = 3, @a = 1);
foo($c = 5, $b = 3, $a = 1);
foo(^c = 5, ^b = 3, ^a = 1);
foo(#c = 5, #b = 3, #a = 1);
foo(.c = 5, .b = 3, .a = 1);

I find all of those very clunky at the call point. Functions are often made to be called several times and having to wade through a sludge of ultimately unneeded symbols to understand calls seems like a Bad Idea (TM). It could be okay at the declaration point though, but the lack of symmetry could maybe hurt since it was not a keyword but a sigil ?

As said by Tom-Phinney, . has an advantage though:

I like the leading point (.) because, for me, it implies that the following parameter name is interpreted with respect to the called function name. It's clearly not method syntax, but for me it does have a somewhat-similar mental model of name scope.

I find that advantage quickly negated on multi-lines call though:

some_long_function(
    unnamed_very_long_struct_decl { ... }, // < oups there was a comma here
    .arg = (42, 44),
    //   ^ maybe too easy to miss when skimming and think of a function call
)

In this situation, the dot . is a hindrance. What's more, it adds clutter at the call site, which is a big no-no for this RFC.

Completely disallow named arguments for #[no_mangle] and extern

To ensure such functions are still first-class citizens in Rust, this has been rejected. It may prove too difficult to implement or too confusing and named arguments could be completely deactivated for them.

Anonymous types (Structural Records) and type deduction and named types

fn foo<T>({ len: usize, f: Fn(T) -> u32 }) -> u32;

This does not allow to differentiate a public and a private name, removes ordering and adds boilerplate ({}). In case of type deduction (foo(_ { a: 3, b: 6 })) the boilerplate is even worse, it asks for an entirely new type, which must now be made public and documented if your function is public.

Builder pattern

let mut vec = vec![1];
vec.reserve_exact(10);

Is this parameter additional or total capacity ? The name of the method is quite unclear here, and a builder would be very heavy, as would a new type.

On the other hand you can have a builder make use of named arguments:

let my_conn = ConnectionBuilder::new()
    .delay(seconds: 4)
    .port(443)
    .build();

Use an attribute

#[with_named_arg] // or something else
fn foo(a: i32) -> i32 { a * 2 + 4 }

let b = foo(a: 42);

While this is very readable at the call site, it is somewhat heavy at the declaration point, does not allow mixing named and unnamed arguments (it could with something like #[with_named_arg(a)], even heavier) and it adds even more bike-shedding: what do we call this attribute ? What if the perfect name is already taken by a macro from another crate ? It also either remove the opportunity to have different public and internal names or adds a lot of sigil : #[with_named_arg(public_name = long_internal_name)], and probably doesn't compose well with patterns.

Do nothing

Without named arguments Rust is already a very good language. Named arguments are a nice feature and can help with safety and soundness but they are not the greatest thing since sliced bread either and Rust can live without them, as it already has for years.

This has been rejected for several reasons in this RFC, reasons that have been explained earlier (safety, soundness, increased readability outside of IDEs with type hints) but also because the alternatives are either insufficient or too heavy-handed. Named arguments have also been on the "nice-to-have-but-needs-design" list for years. This RFC is just the latest attempt at the "design" part.

Prior art

Discuss prior art, both the good and the bad, in relation to this proposal. A few examples of what this can include are:

  • For language, library, cargo, tools, and compiler proposals: Does this feature exist in other programming languages and what experience have their community had?
  • For community proposals: Is this done by some other community and what were their experiences with it?
  • For other teams: What lessons can we learn from what other communities have done here?
  • Papers: Are there any published papers or great posts that discuss this? If you have some relevant papers to refer to, this can serve as a more detailed theoretical background.

This section is intended to encourage you as an author to think about the lessons from other languages, provide readers of your RFC with a fuller picture. If there is no prior art, that is fine - your ideas are interesting to us whether they are brand new or if it is an adaptation from other languages.

Note that while precedent set by other languages is some motivation, it does not on its own motivate an RFC. Please also take into consideration that Rust sometimes intentionally diverges from common language features.

Past Rust-only considerations

This subsection focuses on the discussion that happened in the Rust world about named arguments.

DISCLAIMER: I did not read everything in details, it is possible I missed things. I at least skimmed quickly over all of those listed below.

Relevant discussions

Here are some past discussions on IRLO and past RFCs:

  1. The Wishlist issue says named arguments (and other features) are thought about but the design space must be studied before rushing into one solution that will later prove insufficient or even plain wrong. Even if this RFC is not approved I hope the section below about other programming languages listing the different possibilities chosen by others will help future RFCs that will attempt to tackle this or something similar.
  2. The (I think) first RFC to propose them introduced them in conjunction with default parameters and was closed as postponed. Some remarks on this RFC raised good points that have also been raised in subsequent RFCs, they will be listed in a section below.
  3. Struct sugar RFC #343: it proposed both a form of named arguments and default parameters. It was marked as postponed and lots of people commenting on the issue wanted less magical sugar.
  4. Keyword arguments #805
  5. Named arguments #2964
  6. Pre-RFC thread for #2964: there was much love for .public_name + = in this thread. Those were not chosen here for reasons explained in [Rationale and Alternatives][rationale-and-alternatives]. This thread and several before also raised concerns about the clunkiness of pub, again argued about in the previous section.

Recurring points

Some arguments and opinions are recurring in most of the links above. I will try to list and summarise most of them here. They are in no particular order.

  • Named arguments make changing function parameters names a breaking change: this is certainly true. That's even the point of named arguments, to have a stable and clear interface to a function call, just like some structs have public members or like enum A { Variant { line: String } } instead of simply enum B { Variant(String) }. This point is often raised to argue about brittle syntax. This can be true if the feature is wrongly thought out and designed and named arguments should certainly make it clear what is named and what is not so that programmers can be sure they are not breaking the public interface of some function in a minor version change. But this argument is also false. Named arguments as proposed here do not break existing Rust code one bit, because the public name is separate from the internal binding. If both were always shared, then yes the feature would be error-prone, but they are not, for exactly this reason.

  • Named arguments encourage less well thought out interfaces: I do not think any conclusive evidence has ever been brought to light about this point. On the other hand, the opposite has been extensively studied and battle-tested through Swift's version of the feature, which is lauded by practitioners of the language, notably library designers. Another example, from Rust even, is structs. Why is Latitude { x: 42.1, y: 84.2 } (instead of Latitude { 42.1, 84.2 }) seen as good if named arguments are not good ? To go further, why even name types ? We only need to know the type layout after all, and then we can access all of its data through offsets and dereferencing (such a language does exist, it's called Assembly).

  • Use a (builder) type instead: this argument is counterproductive to me, here it is in another form: why would you use (especially generic) functions when macros can do the job and more well enough ? Types (and builders) have their uses and they can be used in conjunction to named arguments, they are not opposites, just like macros and functions nowadays.

  • Suppose named arguments are allowed, soon people will ask for arbitrary argument order and optional arguments: they are different features. One being accepted is not a sign of the other being accepted. An example is inheritance in today's Rust. Traits can be subtraits (DoubleEndedIterator: Iterator) but types cannot inherit other types and this has never been accepted before when people asked for it.

  • We would benefit far more from reducing the boilerplate involved in the builder pattern: the builder pattern is not opposite to named arguments. Named arguments will not help you when there are 13 parameters to handle for a function input. A builder pattern will be overkill if there are only two usize parameters.

  • Developers need to memorize what arguments are positional and cannot be named in function calls, and what arguments are named: this is true. The response is that code is read far more than it is written. When a choice has to be made between the writer and reader this should be taken into account. Named arguments incur a cost of a minute or two of thinking at most in the majority of cases from my experience in Python 3 and Swift. They can save dozens of peoples hours of debugging and reading documentation. In today's Rust you need to remember the name of a struct's field to initialize it but I have not seen people complain about it either, despite the fact that there is no difference between the private and public name of the field. What's more, autocompletion exists and is used by a lot of people. It is quite simple to autocomplete a function call with named arguments: instead of writing myfunction(<cursor>), write my_function(at: <cursor>).

Named arguments already exist in Rust

This is minor but consider the following:

trait Restriction {
    type Inner;
}

trait Database: Restriction<Inner = u32> {}
                         // ^^^^^ This is a type-level named argument

fn one_string_to_bind_them_all<I: Iter<Item = String>>(i: I) -> String { /* ... */ }
                                    // ^^^^ This is another

Overloading already exists in Rust

Overloading is already available, from a certain point of view, in today's Rust, with two main ways to achieve it.

The first is with members and methods:

struct Sizes { data: Vec<usize> }

impl Sizes {
    fn data(&self) -> &Vec<usize> { &self.data }
}

let data_1:  Vec<usize> = some_sizes_1.data;
let data_2: &Vec<usize> = some_sizes_2.data();

The second is with modules (and crates, since they behave as modules for this):

mod a { pub fn data() -> usize { 42 } }

mod b { pub fn data() -> &'static str { "42" } }

let from_a: usize        = a::data();
let from_b: &'static str = b::data();

This one can even be argued as reverse form of named arguments: the function name is the same and a marker (here the module's name) is used to differentiate.

There is a third one that is nightly-only for now (taken from this blog post):

// required to implement a function with `extern "rust-call"`
#![feature(unboxed_closures)]
#![feature(fn_traits)]

struct Multiply;

#[allow(non_upper_case_globals)]
const multiply: Multiply = Multiply;

impl FnOnce<(u32, u32)> for Multiply {
    type Output = u32;
    extern "rust-call" fn call_once(self, a: (u32, u32)) -> Self::Output {
        a.0 * a.1
    }
}

impl FnOnce<(u32, u32, u32)> for Multiply {
    type Output = u32;
    extern "rust-call" fn call_once(self, a: (u32, u32, u32)) -> Self::Output {
        a.0 * a.1 * a.2
    }
}

impl FnOnce<(&str, usize)> for Multiply {
    type Output = String;
    extern "rust-call" fn call_once(self, a: (&str, usize)) -> Self::Output {
        a.0.repeat(a.1)
    }
}

fn main() {
    assert_eq!(multiply(2, 3), 6);
    assert_eq!(multiply(2, 3, 4), 24);
    assert_eq!(multiply("hello ", 3), "hello hello hello ");
}

What about other programming languages ?

Rust does not exist in a vacuum and a lot of people have thought about named arguments for their preferred language. This section will look at what other languages have done and how (and if) they solved the problems that named arguments attempts to solve.

Since many languages have them in some form or other this will be more of a list presenting the different options with a summary at the end, not a list of functionalities per language.

Note that languages may appear in several categories: they are not exclusive. All languages ever created are not listed and it is entirely possible a solution (and the language using it) was missed. If you find that is the case, please signal it to help improve this section.

Languages that have direct support for named arguments will not be listed in the other categories, even if they fit in them.

Source: Rosetta code

Named arguments through comments

Some examples found in LLVM code (ex1, ex2, ex3):

config::Provider::fromYAMLFile(UserConfig, /*Directory=*/"", TFS)

/* Override */ OverrideClangTidyOptions, TFS.view(/*CWD=*/llvm::None)

llvm::sys::fs::real_path(CheckFile, Path, /*expand_tilde=*/true)

All languages that support inline comments can do this. The simple fact this is used at all is telling since it is very easy for such comments to get out of date and become obsolete or even plain wrong but using such a brittle form of named arguments was still deemed necessary and worth the maintenance cost.

All languages that support comments but not inline comments can do this if the developer writes the function call on several lines. The above examples could have been written that way:

config::Provider::fromYAMLFile(
    UserConfig,
    "", // Directory
    TFS
)

// Taking even more space:
llvm::sys::fs::real_path(
    CheckFile,
    Path,
    // expand_tilde
    true
)

I do not know of a single programming language that does not support the second form, even COBOL can do it.

Named arguments through direct types

This section is for languages that do not have named arguments directly but can use external types to emulate them. Pretty much all languages can do that so this section is only for languages that do not have named arguments directly.

An example in C:

#include <stdio.h>

struct OpenPortOptions {
    unsigned int port;
    unsigned int max_connexions;
};

void open_port(const struct OpenPortOptions options) {
    printf("port: %d\n", options->port);
    printf("max connexions: %d\n", options->max_connexions);
}

// Call with:
int main(void) {
    // Short version, needs a cast, cannot be reused, avoids declaring a local variable.
    open_port((struct OpenPortOptions){ .port = 12345, .max_connexions = 10 });

    // Long version, can be reused if necessary.
    const struct OpenPortOptions options = { .port = 12345, .max_connexions = 10 };
    open_port(options);
    // It is possible to invert the order of the members.
    const struct OpenPortOptions options_2 = { .max_connexions = 10, .port = 54321 };
    open_port(options_2);
    return 0;
}

In such languages an external type is often necessary to implement named arguments. If you have functions taking another set of arguments, you will need another type. Depending on the language, types can be created inline (call(MyType { name1: val1, ... }), as in Rust) or not, which adds more boilerplate (as in the long form of the C example above). Most statically typed languages needs the type to be explicit too, adding more boilerplate.

In some languages, like JavaScript, creating an external type is not necessary, passing an anonymous type with the expected properties is enough. This reduces the boilerplate, at the cost of clarity: expected properties must be documented, often without the help of the language to check for their existence before the function is called.

While this solution works when it is only needed for a few specific functions, it does not scale well, especially if the concerned functions are public: the helper types will have to be too, widening API surface and adding boilerplate for every user of the library, not just the author.

Languages using this method

  • ALGOL 68 (not known to be used in any real code though)
  • Bracmat
  • C
  • C++
  • Forth
  • Go
  • Haskell
  • JavaScript
  • jq
  • Lingo
  • Perl
  • PHP
  • Rust
  • StandardML
  • Tcl
  • Wren

Named arguments through builder types

Some languages cannot use the previous method and must use a Builder pattern to get a form of named arguments, as in the (shortened) Java example below. As with the last method, pretty much every language can use this method. The list below will only consider languages that cannot apply the previous (shorter) method nor have named arguments.

processNutritionFacts(new NutritionFacts.Builder(240, 8)
                                        // What are the units used below ?
                                        // The builder is only partially
                                        // helpful here.
                                        .calories(100)
                                        .sodium(35)
                                        .carbohydrate(27)
                                        .build());

This is often boiler plate-heavy (a builder type and an option type are needed) and just transmit part of the problem to the builder type itself (as in the Builder(240, 8) call above).

It also often easier to forget to make some call if the builder type is not a state machine but a simple storage for optional values. On the contrary having a state machine can force certain calls even when they are not used by the method using the result of the .build() call.

Builder types are very appropriate for more complex configurations but will quickly become heavy boilerplate for two-parameter methods. What's more, builder pattern are often not used internally, only in public facing APIs. Private functions and methods should not be left on the side just because they are private, but they should also not incur heavy maintenance costs of boilerplate just because the developer wanted to write something safe and self-documenting.

Languages using this method

  • Java
  • C# (before 4.0)

Optional and/or unordered named arguments

This section is for languages that have named arguments but function calls can choose whether to use them or not.

An example in Python 3:

def open_port(port, max_connexions):
    print(f"port: {port}")
    print(f"max connexions: {max_connexions}")

open_port(12345, 10)
open_port(12345, max_connexions=10)
open_port(max_connexions=10, port=12345)

# This line will produce an error but this is not the case for all languages
# open_port(port=12345, 10)
#
# SyntaxError: positional argument follows keyword argument

When languages have this feature, using named arguments is left to the user, not the author of a library but it is library authors that are responsible for the names and changing them is a source-breaking change.

This places additional burden on library authors without offering them much: since users can just ignore the names authors are not able to rely on them to help with clarity and must instead often design around them.

What's more, order is often not important as long as the names match which means a reviewer has to be careful when two parameters are named similarly: there could be an uncaught bug hiding.

From what I could find some domains force the use of named arguments when they are available, like Ada in military programs though I did not find conclusive proof one way or the other.

Languages with this feature

  • Ada
  • C# (since 4.0)
  • Common Lisp
  • Dyalect
  • Elixir
  • Factor
  • Fortran
  • Julia (needs a ; in a function parameter list, the names after are mandatory when calling)
  • Kotlin
  • Lasso (only unordered, not optional)
  • Lua
  • Maple
  • Meson
  • Modula-3
  • Nermerle
  • Nim
  • Oz
  • Phix (named arguments must occur to the right of unnamed arguments but order does not matter)
  • Php 8
  • PowerShell
  • Python (2 & 3)
  • R (will fill missing named arguments with unnamed arguments in the given order, very brittle)
  • Racket
  • Raku (seems to be the same behaviour as R)
  • Ruby
  • Scala
  • Sidef
  • Standard ML
  • Suneido (same as Phix)
  • Visual Basic

Mandatory and ordered named arguments

This section is for languages that have the strictest form of named arguments: mandatory and ordered.

func open_port(_ port: Int, withMaxConnexions max_connexion: Int) {
    print("\(port)")
    print("\(max_connexion)")
}

open_port(12345, withMaxConnexions: 10)

Depending on the language there is a public name for the argument or not. When this is not the case named arguments are often seen as a burden because having to call sin(x: x) is redundant and brings nothing in term of informations while still pushing the burden of increased API surface on maintainers.

On the other hand, users of languages with named arguments that have both a public name and a private name (AppleScript, Objective-C, OCaml, Swift) often seem to miss them in other languages for both their clarity and convenience. The ability to turn them off is a must though, as seen in the sin(x: x) example.

Languages using this feature

  • AppleScript
  • Objective-C
  • OCaml
  • Python
  • Swift

More on Python 3 and Swift

Python 3 and Swift are special and very informative examples because they are two languages where it was possible to write named arguments only functions but the languages moved in opposite directions: Python 3 gained a way to remove them in PEP 570 while Swift continues to use them more and more and functions in Swift are documented with the name of their arguments: open_port(_:withMaxConnexions:).

Note: while this section concentrate on Swift, it must be noted that OCaml provides the same features using a different approach. See this link for more informations and an example. The example of Swift has been chosen because it is more widely used than OCaml and closer the C-family of languages, ensuring its syntax will be understood even by non-practitioners, and especially the Rust community that is the target of this document. The same goes for Objective-C and AppleScript.

Python 3

Python 3 has had named arguments for a long time, since it was first released. Despite that they are not used much outside of necessity to bypass default arguments and change the nth argument when n-1 has a default value. Many Python 3 functions and methods that are implemented in C behind the scene even forbid named arguments, range being one of the most famous example.

def only_named_arguments(*, arg1: int, arg2: str):
    print(arg1)
    print(arg2)

# Using both names (in whatever order) is the only valid way to call the function
only_named_arguments(arg2="two", arg1=1)

With PEP 570, Python 3 introduced a way to disable named arguments for a function/method call:

def only_positional_arguments(arg1: int, arg2: str, /):
    print(arg1)
    print(arg2)

# Using positional arguments is the only valid way to call the function
only_positional_arguments(1, "two")

Swift

Swift has had named arguments since its inception and they are fully integrated to the language. They affect overload resolution for example so methods are described as Int.isMultiple(of:), not Int.isMultiple.

Swift named arguments are opt-out instead of opt-in. This was possible because they were here from the start, which is obviously not a possibility for Rust.

Below are all the possible ways for Swift named arguments to work in the language:

// - `range` is both the public and internal name, it must be used when calling
//   the function and when writing its implementation.
func random(range: Range<Int>) -> Int {
    var g = SystemRandomNumberGenerator()
    return Int.random(in: range, using: &g)
}

// - `in` is the public facing name, usable only when calling the function.
// - `range` is the internal name, usable only inside the function.
func random(in range: Range<Int>) -> Int {
    var g = SystemRandomNumberGenerator()
    return Int.random(in: range, using: &g)
}

// - `_` is a placeholder used to note the function is called without a named
//   argument. `range` CANNOT be used when calling the function.
// - `range` is the internal name, usable only inside the function.
func random(_ range: Range<Int>) -> Int {
    var g = SystemRandomNumberGenerator()
    return Int.random(in: range, using: &g)
}

Unresolved questions

  • What parts of the design do you expect to resolve through the RFC process before this gets merged?
  • What parts of the design do you expect to resolve through the implementation of this feature before stabilization?
  • What related issues do you consider out of scope for this RFC that could be addressed in the future independently of the solution that comes out of this RFC?

Defaults parameters

Default parameters and named arguments are often cited together but in reality they are quite orthogonal features. They compose well together in several languages but that does not means they are inseparable from a design point of view.

Whether they should be added to Rust or not should be considered in another RFC.

Allowing keywords

See [Allow Keywords][allow-keywords] in Future Possibilities.

Interactions with closure

One special case that comes to mind is closure and the Fn family of traits (with an example from the Nomicon):

struct Closure<F> {
    data: (u8, u16),
    func: F,
}

impl<F> Closure<F>
    where F: Fn(arg: &(u8, u16)) -> &u8,
{
    fn call(&self) -> &u8 {
        (self.func)(arg: &self.data)
    }
}

Should this impl also be valid for Fn(&(u8, u16)) -> &u8 ? Or Fn(other: &(u8, u16)) -> &u8 ?

I would argue yes, since [Calling a function with named arguments indirectly][calling-a-function-with-named-arguments-indirectly] shows us names can be cast away when needed: not allowing it would be an unnecessary papercut.

I would also argue no: named arguments should be considered like specified generic arguments: ĂŚ: Iterator<Item = u16> does not accept I: Iterator<Item = u8> so impl depending on named arguments should take them into account.

This can be considered a form of specialization maybe, and so out of scope for this RFC. I do not know the internals of rustc enough to know about how the Fn traits are implemented.

Future possibilities

Think about what the natural extension and evolution of your proposal would be and how it would affect the language and project as a whole in a holistic way. Try to use this section as a tool to more fully consider all possible interactions with the project and language in your proposal. Also consider how the this all fits into the roadmap for the project and of the relevant sub-team.

This is also a good place to "dump ideas", if they are out of scope for the RFC you are writing but otherwise related.

If you have tried and cannot think of any future possibilities, you may simply state that you cannot think of anything.

Note that having something written down in the Future Possibilities section is not a reason to accept the current or a future RFC; such notes should be in the section on motivation or rationale in this or subsequent RFCs. The section merely provides additional information.

Allow keywords

write(in db: Database) is not possible with the design proposed in this RFC.

We could allow keywords to be used in named arguments since they can never be ambiguous in such a case, with the exceptions of pub, ref and mut, thanks to the:

  • two identifiers back to back at the declaration point
  • : separating the argument name from the expression at the call point

Using keywords would be prohibited with pub since it would allow the following:

fn with_kw_as_named(pub in: String) {
    // ERROR: `in` would be a variable here
}

Variadic functions

In Swift, named arguments arguments are used as boundaries to allow for functions with several variadic parameters. While such functions do not yet exist in Rust (and there is no hard requirement for them to do), this opens up the possibility:

// Some made-up syntax, not a proposition
fn add_sub_several(pub add: usize..., pub sub: usize...) { /* ... */ }

add_sub_several(add: 1, 2, 4, sub: 3, 5);
34 Likes

Can you say more about why not? (I didn't spot it in the rationale/alternatives section.) For example, Rust could have required that you initialize named fields in a struct in the same order as the definition, but it doesn't. Why couldn't you put named parameters to a function in an arbitrary order too?

(Certainly positional parameters can't be reordered, like in a tuple-struct initializer. But when things are named...)

11 Likes

Good catch, thanks !

My reasoning is that allowing unordered named arguments is:

  1. Inconsistent with generic parameters
  2. Impossible with the proposed form of overload: func(a:b:) and func(b:a:) would be different functions
  3. It makes it harder for the compiler, rust-analyzer and more importantly humans to quickly understand what function is referred too, and what argument too, especially since I propose allowing mixing with unnamed arguments
  4. It makes autocompletion strange: it would probably autocomplete in definition order but if a call one line above changed the order for reasons, reading the code would become inconsistent

How so? It's not Vec<T = i32>; they're positional, not named. And if you mean things like where T: Foo<A = i32, B = u32>, that can be reordered to where T: Foo<B = u32, A = i32> and it still works.

That's fixable, though, no? You could, for example, sort the named parameters to make the overload key.

TBH, I think it's very much a misfeature if both fn func(pub a: i32, pub b: i32) and fn func(pub b: i32, pub a: i32) can exist in the same scope, so that func(a: 0, b: 1) and func(b: 1, a: 0 do different things.

I'm puzzled by this one, because to me that's an argument that we shouldn't have this overload-by-parameter behaviour at all. I don't think one can "quickly understand" which function it is if, say, the only difference is the name of the third of seven parameters, for example.

Is that any different from, say, struct initializers? It's not even a warning to Color { r: 255, b: 255, g: 0 }, for example.

Basically, my core thing here is that if something is nominal, I expect the names to be the thing that matters. Not that positions have to matter too.

(Like it would be really weird if we said "no, you must match those enum variants in the same order as their definition".)

26 Likes

As per usual with the topic of named parameters, the motivation is entirely false and fails to consider the nature of Rust being a static typed language. The fact you give an example in Python is very telling.

The languages where the above makes sense are all dynamic typed. In Rust the way to define a proper readable interface is to use the type system, not by naming the parameters. Give the parameters proper meaningful types that provide semantic meaning using the new type idiom.

Correct idiomatic design in a static language, using existing features is therefore something like:

fn open(port: Port, max_connections: i32, timeout: Seconds) { ...}

open(Port{8080}, 2, Seconds{60})

Also note that you have a typo in the parameter name which makes this idea extra obnoxious. Your typo would be part of the official interface and cannot be fixed in a backwards compatible way!

Major thumbs down.

4 Likes

First, this RFC is very well-written, one of the best I saw. Thanks!

I have two concerns with this, however:

  1. Overloading. While there is motivation to support it, it is quite big feature, and I don't think it is necessary in Rust because it has very good generics support. I would argue that I prefer fn new() -> Self, fn with_foo(foo: Foo) -> Self, other than even fn new() -> Self, fn new(pub foo: Foo) -> Self. I strongly feel that if anything, this should be left as a future possibility.
  2. Complexity. While this feature is motivated, it is quite big change and I feel like Rust already exhausted its complexity budget. I'm afraid that adding more features that aren't necessary (i.e. cannot be worked around, and this apply to almost all syntactic features) or required almost everywhere, is just too much.

And last note, did you repeat the RFC template in each section? I find it hard to read.

12 Likes

I like that you're looking into named arguments. I think it's something that many people want, but it's really difficult to come up with a good design.

Some concerns about your proposal:

  • Parsing ambiguity:

    fn foo(name (a, b): name); // currently parsed as a tuple struct pattern
    
  • Parsing ambiguity with type ascription:

    foo(name: Foo);
    

    Yes, type ascription is unstable, but my understanding is that we want to stabilize it eventually.

  • pub is currently used only for visibility. You're proposing to overload it's meaning. This wouldn't be the first time a keyword is overloaded in Rust; for example, const has at least 4 meanings. However, that makes Rust harder to learn and should arguably be avoided.

    Note that it would be possible to add a contextual keyword. For example, fn foo(kw bar: i32) would be unambiguous. Or, we could just repeat the name, that's how it is done in Gleam.

  • Default parameters are out of scope, but overloading is included. I think this is the wrong way around: Overloading is not a good feature, it has a much bigger potential for confusion and subtle bugs than default parameters.

    Yes, default parameters are also a limited form of overloading, but it's less hazardous: When you omit or add an argument, you can be sure that you're still calling the same function. There aren't multiple functions with the same name that can potentially do entirely different things.

    And if you were going to say, function overloads with the same name should do the same thing, that's not always how overloading is used in practice. And even when overloaded functions should behave the same, it's possible for bugs to sneak in. These are reasons why many experienced Java devs hate function overloading with a passion.

    Also, function overloading goes against the DRY principle. And in the vast majority of situations, it is just used for providing default arguments.

  • The twos(x:_:) syntax isn't intuitive, it looks really confusing. Another reason why I think that function overloading isn't a good idea.

  • impl Fn(add: f32, mul: f32) -> (f32, f32) is problematic. Fn is a trait, so you need to consider how its declaration should change. Fn(f32, f32) -> String is syntactic sugar for Fn<(f32, f32), Output = String>. How should the syntax you proposed be desugared?

  • You haven't mentioned function pointers. Should fn(add: f32, mul: f32) -> (f32, f32) types be allowed as well?

  • I agree with @scottmcm that it should be possible to specify named arguments in any order, which is consistent with existing Rust syntax. Positional arguments should be forbidden after named arguments, so positional arguments always appear at the same position.

  • The RFC is unclear about the question whether named arguments must always be named at the call site. There's this sentence:

    Of course functions where the arguments are already clear do not have to use them: sin(x: x) would be ridiculous and redundant.

    Which seems to contradict this statement:

    You cannot omit named arguments, even when the passed expression is exactly the same as the identifier: my_db.register(name: name) cannot be shortened to my_db.register(name) .

    Making named arguments mandatory is a valid trade-off, but you should be aware of the pros and cons, and you should draw attention to them in the RFC. The main disadvantage of making them mandatory is backwards compatibility: If argument names are mandatory, they can't be added to existing functions without a new major version. Most importantly, they can't be added to existing functions in the standard library.

19 Likes

An alternative syntax for argument name renaming is using the @ pattern.

pub fn register(pub to@db: Database) { ... }

Without pub, this is already accepted as a pattern, and only needs one special case. Also, there is no syntax ambiguity in this syntax.

Also, patterns can be allowed with the named argument naturally:

pub fn func(pub to@Struct { foo, bar }: Struct)
3 Likes

(Comments are procedural and do not indicate a preference for/against the feature. I speak for myself, not wg-grammar.)

mut/ref are part of the binding pattern. If the syntax is $ident $pat : $ty, then it should be named mut name: String. This is also why it's Wrapping { mut inner } and not mut Wrapping { inner }.

Wait, is this suggesting that if a single member of the overload set exists, you can call it without argument names? This goes against what is said elsewhere. Also, I seem to recall something like this being mentioned in Swift Regrets (but searching through, I can't find it).

No, $ident : $ident can be either, so it is in fact ambiguous. (Disallowing type ascription or requiring the $ident : [...] to be a named argument would "resolve" this, though perhaps suboptimally.)

_ is not an identifier, so this falls out of other definitions. However, it is probably good to emphasize.

Probably best to just forbid use of #[no_mangle] with argument names, and require the use of #[export_name] instead.

Dead link.

It's not allowed in Swift, the go-to example of "doing named arguments right," so that's a decent example for why it's not needed. It also just straight helps in API design to know the order that is used. It's always possible to make them order-independent later, if the nominal quality outweighs the API design considerations.

It's the syntax used by Swift (and, IIUC, derived from Objective C), so it has precedent. People already use atan2(f32, f32) informally, so providing atan2(y:x:) is a "simple" extension to that.

I believe this is the point of including argument-name-based "overloading" in this proposal: in addition to fn Vec::reserve(&mut self, usize), we can add the new method Vec::reserve(&mut self, additional: usize).

This allows overloading to take advantage of named arguments for inherent methods, but any existing traits cannot take advantage of named arguments[1].


If named arguments are added to Rust, I would like to see edition 20XX adopt Swift's approach, where fn foo(name: Ty) is fn foo(name name: Ty), and fn foo(_ name: Ty) is used for an unnamed parameter. Obviously, previous editions would have fn foo(name: Ty) be fn foo(_ name: Ty) for compatibility, but linted against. This maybe stretches the edition policy (I think per current understanding the exact same syntax can't be valid in two different editions and mean different things?) and makes reading old edition code potentially surprising, but it is 99%[2] a static error to get it wrong.


  1. At least, not without additional features, such that you must implement fn a(_:) or fn a(a:), and the other delegates to the one you implemented. ↩︎

  2. It would be possible to construct an overload set that is a) not immediately obvious and b) makes reading old edition code potentially hazardous w.r.t. which overload gets called from a future edition perspective. However, such code would have to be actively written to be confusing and against warn-by-default lints. Rust does not attempt to protect against maliciously written code (although it does like to minimize such underhanded tricks). ↩︎

8 Likes

I found a few relevant entries there regarding named arguments:

  • Swift Regret: using tuples as argument lists. I agree that having to name functions without calling them like add(to:) is really awkward and will be forever technical debt in the compiler.
  • Swift Regret: Labeled Tuple Elements // -dealloc. This one isn't about named arguments specifically, but rather named tuple elements, which seem necessary to address @Aloso's observation about Fn(add: f32, mul: f32) -> (f32, f32) desugaring.
  • Another observation, also on Swift Regret: Labeled Tuple Elements // -dealloc, regards the ambiguity between type ascription and named arguments in let bindings. This one applies in rust even at the use site, and it is the strongest argument against named arguments to me personally (at least with the name: value syntax) since I will take type ascription over named arguments if I had to only pick one.

I've never written any swift code myself, but I have read a lot of it and I noticed that parameters are always written like func foo(_ param: Type) and I had no idea what was up with the _ since it never seems to be anything else. I'm guessing based on this proposal that it has to do with argument names? The syntax is terribly unintuitive and boilerplate-y and I would really like for rust not to adopt it. Using @ patterns seems like a reasonable alternative, which I already use for rustdoc parameter naming (although I think there are issues if pattern matching something that is not Copy).

4 Likes

(Note: this response isn't meant to imply either support for or inclination against named arguments, just evaluating this proposal for them. In general, I'm mildly opposed to named arguments, but not strongly opposed.)

I appreciate the level of detail you've put into this proposal; any proposal for named arguments has to show how it interacts with many different features, such as other bits of argument syntax. I also appreciate that you've addressed some problems from previous proposals; for instance, you're proposing an opt-in for argument names rather than automatically exposing all names as part of the interface. And I like that you're trying to do less, such as not adding defaults or reordering or optional parameters at the same time.

I do think, though, that this proposal is still trying to do too much at once:

  • It's trying to introduce overloading at the same time as named arguments. Overloading is a much bigger ask, and even more debatable than named arguments; I don't think we should do those at the same time. And personally, I'm much more strongly opposed to overloading than to named arguments.
    • As a result, it also has to introduce syntax for disambiguating overloads.
  • It's introducing argument renaming, where an argument has one name in the interface and another inside the function. I don't think we should do this at all. If you want to expose a name for the interface, use that name for the argument, and if you want to call the argument something different inside the function, use a let binding: let db = to;.

Separately from that, I'd like to see more consideration of alternative syntax:

  • = is worth evaluating, given its existing use in println!/format!/etc. Yes, it's reminiscent of an assignment, but that's not necessarily a bad thing; in a way you're "assigning" a value to the named argument.
  • Inferred types, like func(_ { name: value }), would have several advantages, and I don't think they should be dismissed as a well-fitting solution. Among other things, exposing a type has the advantage that you can provide multiple ways to construct that type, it would allow defaulting in various ways already (e.g. ..Default::default()), if in the future we add default field values we'd automatically get default arguments, and having the type available means you have the option of constructing the arguments in a separate statement and then calling the function. And while this would allow reordering arguments, that's not necessarily a bad thing.
  • You mention builders as an alternative but dismiss them fairly quickly. I do acknowledge that in the contexts where named arguments are commonly used, builders might not feel as simple, either to define or to call. But if we're already talking about adding syntax, we could add some additional syntax (possibly via opt-in helper attributes) to enable builders on function types without having to define them; for instance, given a function f, you could write something like f.at(value).(unnamed, unnamed2);, and have that syntax just call the function f(at: T, arg2: T2, arg3: T3). I'm not necessarily proposing that that should be the ultimate solution here, but I think alternatives are worth consideration before we assume named arguments are necessarily the right answer.
21 Likes

The builder pattern is not an alternative for named arguments. It's just too verbose, and it handles non-optional arguments poorly. There are libraries for generating a builder with the typestate pattern that can ensure that .finish() can't be called before all non-optional values were provided, but I don't like pulling in a dependency for this.

To avoid the boilerplate, I usually crate an Options struct instead:

#[derive(Default)]
pub struct CompileOptions {
     foo: Foo,
     bar: Bar,
     baz: Baz,
}

So people can call compile(CompileOptions { foo: Foo, ..Default::default() }). But this is still boilerplate, and also can't handle non-optional arguments, if the struct implements Default. It's also more cumbersome to use because the user has to import the Options type.

3 Likes
  • If argument names are mandatory, they can't be added to existing functions without a new major version. Most importantly, they can't be added to existing functions in the standard library

Idea: All existing editions have a feature, probably limited to functions in std & core, where named arguments can be entirely ignored at the function call site, and the arguments are passed in order, just like they would be called without them.

Example:

fn test(pub x: i32, pub y: i32) {}

// Normally called like this
test(x: 1, y: 2);

// Could be called like this with my proposal
test(1, 2);

And then in the next edition the named arguments would be required just like any other function with named arguments.

After using Objective-C and Swift, I have to disagree with you. I have a lot of gripes with Objective-C, but after working on a very large codebase involving hundreds of other programmers, I've really come to value the named parameters.

I personally don't think of it as overloading, and I'm not sure I'd describe it as such. Swift and Objective-C consider the parameter names as part of the function name. Given this proposal has similar syntax and suggests not allowing reordering of arguments, I'd reframe this proposal as using the parameter names as part of the function name, not overloading.

7 Likes

In fact, the RFC itself offers a counterexample to @yigal100's claim that the type system obviates the need for named parameters:

(emphasis mine) You could create four newtypes, one for each of the last four parameters, but I personally think that would be less readable and would also obscure the fact that all four parameters do in fact take the same kind of filter rule.

When an RFC addresses an anticipated objection directly, I think it's rude to respond by repeating the objection in generic terms, without actually responding to the way the RFC addressed it. It makes it look like you didn't actually read the thing.

[I have no strong opinion on whether Rust should have named arguments.]

18 Likes

Oh no, please absolutely don't. It's great that we finally got rid of ad-hoc overloading in a mainstream, useful language. It's got no place in a system centered around safety and correctness.

I'd like to point out that purely name-based overloading is completely superlfuous because you can just write a different function with a different name. Foo::new(bar: 42) and Foo::new(qux: 43) are not in any way better than Foo::new_bar(42) and Foo::new_qux(43); they are only noisier and create yet another bifurcation point in API design that nobody ever will agree on.

But identifier is an expression too, and there's no way to tell apart expr: expr and expr: type at parse-time. It looks like you are already thinking about typecheck/semanal time, but how does this get represented in an AST? Tying the parser and the typechecker together is not an option.


There's a lot of long-winded self-advertising under the "rationale" section, but you only mention Swift in a single paragraph, and you never address the main pain point there. Swift is a language where named arguments existed very early on, and they have been a massive pain in the neck from the get-go. In particular, they don't interact well with type signatures where one must describe function types with named arguments. This problem is most visible in the case of higher-order functions, which are prevalent in Swift and Rust alike. It is still not clear to this day what the correct solution is to typing such functions:

  • should named-argument functions have the same type as their anonymous counterparts? This competely cancels out any advantage of named arguments, because the indirect call will not (be able to) use named arguments.
  • should there be different signatures, ie. should names be part of the function type? This has the enormous disadvantage that it won't be possible to pass a function with non-public argument names and vice versa, which will create more boilerplate (unnecessary wrapper closures) than needed.

Nobody ever seems to address, let alone solve, this problem. Swift itself has switched sides several times. No named arguments RFC can be taken seriously without considering and proposing a solution.

6 Likes

So by your own logic, this argument-name-based "overloading" is just another function, and it's got no place in a correctness focused language. I apologize, but it really seems like you've contradicted yourself in two adjacent points.

This RFC does not ask for type-based overloading, and I agree that Rust has no need for type-based overloading. However, if Rust were to have argument labels, it only makes sense that argument labels would be part of the function name, and thus "overloading" based on argument labels would be allowed — because it's not overloading at all.

This doesn't quite match my experience. Higher-order functions are an interesting wrinkle in design, but the Swift API's use of argument labels is generally seen positively. And we can learn from Swift here as well.

The standard solution is that labels don't exist in the type system, and are merely sugar for naming the mangled function name. I don't see how that is a failure to solve the problem.


(I still would like to not indicate being for or against this feature and this RFC in particular.)

11 Likes

:wave: Hi, I’m the author of the Swift Regrets articles mentioned above, and one of the early Swift language developers (though no longer). For me, the two articles that got called out above were about the implementation of named arguments more than the feature; I think most of us still think the feature is a good one. That said, the point made above about named arguments being designed into the language and stdlib from the beginning is really important (and it was massively disruptive when we changed the guidelines to what they are today, a few years in).

I miss named arguments in Rust, not just for new_foo and new_bar but for any argument where the type information is not strong enough to clarify the purpose, often &str, Duration, or None. You can newtype these, sure, but if the only reason to newtype is clarity at the call site? Then it’s not pulling its weight.

But this “I miss…” is a snap-my-fingers kind of wish. Adding named arguments to present-day Rust is something that should be approached with caution, and the rules won’t come out exactly like Swift’s (which for better or worse has type-based overloading as well). The migration path for both the stdlib and existing crates out there is a difficult problem. So I appreciate both the thoroughness of the proposal and the thoughtful critiques in the thread.

I do recommend people check out the Swift API Design Guidelines to see what argument names look like in practice in a language that’s had them from the start.

  • On overloading: I agree that the correct user model for labeled arguments is as separate “full names”, not overloading on the “base name”, even though the compiler will have to handle them kind of like overloads for error recovery and partially-written code. The fact that Swift lets you refer to a function by its base name only is not conducive to library evolution, and I wish we hadn’t allowed that. (We never managed to come up with a “full name” syntax for a zero-argument function, though.) I think this means that if labeled arguments are added to Rust, the base name would only refer to the function with no labels, and any full names with labels would have to always be referred to with the labels included. (This implies no overloading on arity without also having labels.)

  • A lot of the troubles we had were in preserving labels in function types. Don’t go down that route.

  • On optionality: Swift differs from Python and C# in that the argument labels are required at the call site if present. Why? There are some technical reasons, but mostly it came down to wanting the same call to look the same everywhere, giving library authors control of how their API is used. It also makes evolution much more straightforward; you don’t need to worry about collisions with other functions in your library. And because people get types wrong much more than they get labels wrong, the labels are important for diagnostics when multiple functions share a base name. Separating the label from the local parameter name also allows them to be different, which is important because good labels are not always nouns.

  • On reordering: This goes a lot with the previous one, but mostly only at the abstract level of “the library author should design their API with intention”. I agree that many times it’s not important, and that it’s somewhat inconsistent with how Rust does structs, and I can’t put my finger on a more concrete reason to disallow reordering besides “it’s simpler for the compiler”.

  • On name: name: this is a Rust feature that I miss in Swift, so if you can figure out how to make a plain name work here I would love it. One way to do so is to disallow unlabeled arguments after labeled arguments, and disallow reordering; that way, you know that if you have another unlabeled argument, it’s actually labeled with the same name.

28 Likes

Does this mean we should allow functions with the same name but different argument number? After all that also isn't type-based overloading.

1 Like

Isn't this already technically possible by manually implementing the Fn traits?

Actually I found the article I read about it: Function overloading in Rust. | Nikodem Rabuliński | The Startup

Though I agree with the author that it really shouldn't be done.