A number of programming languages offer a feature called “Named Arguments” or “Labeled Arguments”, which makes some function calls much more readable and safer.

Let’s see how hard it would be to add these in Rust.

Consider a function such as:

fn rect(x: u32, width: u32, y: u32, height: u32, background: Option<Color>, foreground: Option<Color>, line: Option<Color>) -> Widget {
 // ...
}
rect(X, Y, WIDTH, HEIGHT, LINE, None, None); // Oops, I have completely confused the order of arguments.

It would be nice if we could leverage Rust’s syntax and/or type system help us avoid confusing these arguments. This is a feature often called Named Arguments or Labeled Arguments, which is available in Python, OCaml, Swift, Dart, … with strong typing in some of these languages that support it.

In Rust, one could imagine writing

// In this example, `{( ... )}` means that we're using named arguments.
// Other syntaxes are possible.
fn rect{(x: u32, width: u32, y: u32, height: u32, background: Option<Color> = None, foreground: Option<Color> = None, line: Option<Color> = None)} -> Widget
{
    // ...
}
rect{(x = X, y = Y, width = WIDTH, height = HEIGHT, line = LINE)}; // Arguments are automatically reordered. Default values are automatically inserted. Above error becomes impossible.

Wouldn’t that be more readable and safer?

What this post is about

This is not about syntax, only about semantics.

I was toying with a few ideas on how to implement named arguments without breaking the existing type system. I have a few macro-based prototype implementations. It’s way too early to turn them into a RFC, but I hope that these can serve as a basis for further conversation.

With these prototypes, we can write 1:

// Define the function.
define!(fn(rect, x: u32, width: u32, y: u32, height: u32, background: Option<Color> = None, foreground: Option<Color> = None, line: Option<Color> = None) -> Widget {
    // ...
})

// Call the function, macro-style.
// Arguments are re-ordered as needed and optional arguments are replaced with their default value if needed.
call!(rect, x = X, y = Y, width = WIDTH, height = HEIGHT, line = LINE);

// Or, equivalently, builder-style.
rect::setup()
    .x(X)
    .y(Y)
    .width(WIDTH)
    .height(HEIGHT)
    .line(LINE)
    .call();

// On the other hand, the following fails to type-check, because we've forgotten
// required argument `height`.
call!(rect, x = X, y = Y, width = WIDTH, line = LINE);
// ... or equivalently
rect::setup()
    .x(X)
    .y(Y)
    .width(WIDTH)
    .line(LINE)
    .call();

// And the following fails to type-check because we've used the same argument
// twice:
call!(rect, x = X, y = Y, x = X, width = WIDTH, height = HEIGHT, line = LINE);
// ... or equivalently
rect::setup()
    .x(X)
    .y(Y)
    .x(X)
    .width(WIDTH)
    .height(HEIGHT)
    .line(LINE)
    .call();

A bit more verbose but much safer, right?

At the time of this writing, the prototype only support module-level fn declarations, not functions within impl or trait or closures. I don’t think that there’s any strong blocker for either, I just haven’t attempted implementing these yet. These are left as future works.

The general idea

The general idea is that macro define! creates a type-level final state machines that behaves as follows:

For the moment, all this is implemented by generating one struct type per state S and storing all of them within a semi-hidden module.

Macro call! just chains calls to these functions S::a(...). The type system catches when we’re attempting to use the same argument twice (because the method doesn’t exist) or when we’re missing one argument (because method call() doesn’t exist).

Early conclusions

This is still an early prototype but I believe that it demonstrates satisfyingly that we can extend Rust with named arguments, at least in simple cases, without modifying either the type system or the semantics of the language in depth.

It feels like we could extend the Rust type system to improve error messages (when we use the same argument twice or forget one argument) and likely compile-time performance of this use case without it having it cascade in too many places.

Future works

As mentioned previously, the current prototype only works for module-level function declarations. Making it work for impl-level functions and trait-level functions should be possible but will require a little more effort to store the type-level finite state machine.

Links and credits


  1. Disclaimer: The syntax in the latest prototype is actually slightly more verbose than the snippet at the start of this blog entry. Real-life complications have prevented me from finishing the prototype in which we adopt this cleaner syntax but as far as I can tell, there doesn’t seem to be any Rust-level blockers. ↩︎ ↩︎