Possibly one small step towards named arguments

I'm going to assume that there already is some interest on named arguments in Rust.

I have implemented a very early prototype that seems to suggest that we can implement these arguments in Rust without deep changes to the type system or compiler.

Basically, this lets us write:

// 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();

The short of it is that we handle named and optional arguments by generating a type-level FSM that accepts only sequences of arguments in which all required arguments are provided and no argument is provided more than once. For a prototype, this can be done entirely in userland, at the cost of not-very-useful error messages.

I've posted more details on my blog.

I'd like to gather some early feedback and see if there's interest in me pursuing this work and possibly turning this into a RFC.

edit Fixed typo in example.

6 Likes

The biggest downside IMO is that it doesn't support methods well. I'd want to be able to write a.b() instead of call!(SomeType::b, a).

You'd still be able to a.b().arg(value).more(another_value).call(). I think if a macro like the aforementioned one got support it could graduate from macro level, at which point it could support a macro-less version.

In other words, yeah the macro isn't terriblely pretty for methods or trait methods, but I think it paves the way for cleaner usage there.

Great work. I got about as far as you and sputtered out (https://crates.io/crates/rubber_duck). I took a slightly different take on things, but the underlying approach I think was the same: generate a builder where each method can only be called once. Here are my notes:

  1. In my implementation, you could have positional arguments as well. Those I exposed as a .next(positional_arg_value) method on the builder - the named argument setters weren't exposed until you had set all the positional arguments. It's a lot less pretty for you syntax, but it made writing the equivalent of your call!(...) macro a lot easier to write wrt. positional arguments. Though I wrote mine as a macro_rules. I'd personally suggest making it a proc macro, and then putting positional args as builder constructor arguments if you do want to support positional arguments. I got away using the bland build method of next because I wasn't directly exposing the builder. I exposed a generic caller macro and the original function, whereas you expose the generical caller macro and the builder.
  1. Macros and structs are in different namespaces. This lets you have a bit of fun on nightly for functions. You can generate a macro for each function in define! that is named the same as the builder. Basically you could expand you support on nightly (because macro expansion to macro definition is only on nightly, IIRC) to not only support call! and the builder syntax, but a unique macro per builder. E.g. in you example you'd be able to generate a macro to support rect!(x = X, y = Y, width = WIDTH, line = LINE);

  2. Yeah, impl methods and trait methods should be possible.

I'm personally pushing for a more a struct sugar based support, which is what my crate was pushing for. I'm you took the time to implement something, because I think it's nice to have several different takes on how things should look and behave. It's big problem space, and I think macros are a nice way to explore it, gather feedback and eventually contribute to consensus.

Why not

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

?

"When you assume you make an ass of U and Me"

It is fine to have your own macros. Adding features to Rust, however, which is already a complex and quite large language, must pass a very high bar of showing that the new feature is worth the added complexity.

Named arguments do NOT pass that high bar for Rust nor for any other statically typed language. Not every feature that exists in e.g. JavaScript needs to be tacked onto Rust. The appropriate and idiomatic way in any statically typed language such as Rust is to leverage the type system for API design.

As an aside, (because people always brings the following examples):

  1. While C# is a statically typed language it has named arguments - True, however, this was done by microsoft only to support their legacy COM interfaces. New APIs introduced in C# should be strongly typed instead, according to the official MS docs.
  2. What about Objective-C & swift? Well, Objective-C was a horrible hybrid between C and SmallTalk. The feature was brought from SmallTalk which (no surprise there) is a dynamically typed language just like JavaScript. Swift in turn has the same feature for backwards compatibility.
2 Likes

That is actually a typo in my text, my bad. Fixed.

Indeed. I do not pretend to claim that this feature must be implemented. Only that there is interest, as you can check on the RFC repo.

The humble objective of my experiment is to determine, if named arguments are implemented, how deep the changes to the language and its semantics need to be (at this stage, I don't care about syntax). And the answer of this experiment is: apparently, only skin-deep, although error messages might make things more complicated.

Which is good news, when you consider that OCaml (afaict the first language with strongly-typed named arguments) needed a fairly serious rewrite of its type system to add this feature ~20 years ago.

2 Likes

There definitely is interest. You assume correctly.

There's 700+ comments to read there, probably more. As for why things haven't been implemented - there's no consensus on how, or even if they should be implemented. That's why I think macro explorations of this are useful.

I like this summary best of how hairy of a topic this is:

10 Likes

Beyond macro implementations, I think the best thing one could do to promote the eventual inclusion of named arguments would be to spin up a GitHub repo containing mulitple RFCs going down various routes. There's a lot of options. I think it'll be very hard to drive consensus without thoroughly exploring the options.

1 Like

At this point it could as well be an attribute-like proc macro.

Inspired by this really interesting prototype, I gave a go at making an attribute proc macro - I just made a thread to discuss it, and would welcome your thoughts! :slight_smile: