[Pre-RFC] Named .arguments

Actually, you can't have a struct and a function with the same name, so it would be fine. But I don't like the syntax very much, function calls shouldn't look the same as structs.

For alternative ideas, it might be better to discuss them in the other thread about named arguments.

2 Likes

Yes you can.

In fact, that's how tuple structs are already implemented. And they look exactly like function calls, so I don't see why function calls shouldn't look like structs.

Oh, I tried it with a unit struct (unit structs (and tuple structs) can't have the same name as a function). Well, that's another reason not to do it IMO.

1 Like

Overall, I agree with other posters: this RFC needs to be way more bulletproof. Named arguments are always strongly controversial (I think the civil war on the dlang forums is still ongoing), so any proposal needs to be well defended.

The major contention points I'm aware of are:

  • Semver implications.
    • Generally speaking, if you add named arguments to your functions and then change the names, you're breaking semver. This is something some people are very worried about, because they're concerned that named arguments will be on-by-default, which means they'll be locked into name choices by an external decision; your proposal is opt-in, and doesn't have this problem, which you should emphasize.
    • Even then, some people have a knee-jerk reaction against any change adding semver "attack surface", so you could probably add a disclaimer that goes "this adds a semver constraint that names stay the name, but it's a kind of constraint that already exists with struct fields".
    • Also, the "arg name vs arg pattern" distinction mitigates the above problem, which is something you should draw attention to.
    • There is a trade-off between enforcing that named argument be passed with their names, or allowing them to be passed as positional. Enforcing "namefulness" makes the style more consistent, but the benefits of that are dubious. Allowing positional use of named arguments allows existing APIs to switch to named arguments without breaking semver, which is good for adoption. Personally, I don't think the compiler should worry about style consistency; this should be a clippy lint at most.
  • Expressiveness
    • A common objection to named arguments is that they don't improve expressiveness/readability compared to other existing or proposed features, such as enums and structural records. For instance, it's often pointed out that the following:
      Window::new(.title = "Hello", .w = 300, .h = 200)
      
      could instead be written as
      Window::new(Window::Title("Hello"), Window::Size(300, 200))
      
      The proposal should demonstrate why these existing features aren't adequate for many use cases.
    • Another common substitute for named arguments is the builder pattern. Personally, I find it absolutely ugly, but it's an accepted pattern for languages where named arguments aren't available. The proposal should explain why named arguments provide strong added value over that pattern. (mhh... I'm thinking I might post an article about this on medium at some point)
  • Language direction
    • People discussing named arguments are sometimes worried that they might encourage bad coding practices. The idea is that, by adding optional expressivity features to the language, we might make the language less expressive for people who don't use that feature. Eg:
      select_until(savedCursor)
      
      might be replaced by an API like
      select(.until: savedCursor)
      
      but careless users might instead use
      select(savedCursor)
      
      which is less explicit for people reading the code. In practice, these concerns can probably be somewhat tempered by including formatting guidelines in the proposal, with some examples.
    • There's a general undercurrent in a lot of discussions of "is this really worth lang team effort, given that newtypes / the builder pattern / structural records / IDE annotations already exist?" I think part of the process of championing named arguments is to record existing support of the notion, and frontload that support to remind everyone that, yes, this is something people want.

All that being said, the more I write about this, the more I think this is a debate that can only be solved by the lang team.

There's already been way too much bikeshedding. What the community needs now is for the lang team to do one of three things:

  • Declare "we want the feature to be implemented, and we will implement it this way, for these reasons". I think there's a lot of trust in the lang's team design decisions, and if they pick a design the bikeshedding will mostly stop.
  • Declare: "we probably want the feature to be implemented, but we're not sure about X and Y; there are multiple possibilities, but in the end we want X and Y to be designed in a way that meets criteria A, B and C"; then focus discussion on criteria A, B and C.
  • Decide that named arguments aren't a priority, and any named argument proposal during the next N months/years will be automatically rejected.

This feels like a good fit for the MCP process.

@Ixrec @CAD97 thoughts?

22 Likes

Thanks, this is very helpful input!

I plan to add the requested information and then submit the RFC on GitHub, so the lang team can decide how to proceed.

If the lang team likes the RFC but wants to prioritize other things first, they can postpone it.

What is that?

This second issue is exactly why mandatory named arguments are beneficial: you can design the API with argument names as a meaningful part of the API readability surface.

In any case, I'm mostly in the camp that because Rust's standard library is already stable and doesn't use named arguments for API expressiveness (like Swift's does, as it was designed from the start with named arguments), named arguments are always going to feel bolted on.

It's for that reason my position still is that working on making "options structs" more ergonomic is a better short-term goal than full named arguments.

Major Change Proposal. Rather than an RFC, this is a proposal more focused on "we should do this" and general direction of the change than specifics. It's also primarily aimed at the compiler/lang teams rather than the more genral audience for RFCs, so it can assume a more knowledgeable starting point.

If an RFC lays out the design of a feature, an MCP is laying out goalposts for a desired change in the compiler/language.

3 Likes

The downside of a clippy lint is that the lint must be allow-by-default, or must be disabled by projects that use #![deny(warnings)], so named arguments can be added to crates backwards-compatibly without causing warnings.

I recently came across the perfect use case for named arguments. It's a module to calculate sunrise and sunset times depending on your latitude and longitude. The module is full of functions similar to this:

/// Calculates the hour angle (in radians) at the location for the given angular elevation.
/// - lat: Latitude of location in degrees
/// - decl: Declination in radians
/// - elev: Angular elevation angle in radians
fn hour_angle_from_elevation(lat: f64, decl: f64, elev: f64) -> f64 {
    let term: f64 = (elev.abs().cos() - lat.to_radians().sin() * decl.sin())
        / (lat.to_radians().cos() * decl.cos());
    let omega: f64 = term.acos();

    omega.copysign(-elev)
}

Its readability would really profit from named arguments, whereas newtypes would just make it more verbose I believe.

Is this something I could include in the RFC? It is not the most representative Rust code after all.

2 Likes
hour_angle_from_elevation(.lat = lat, .decl = decl, .elev = elev)

This doesn't look like it's doing much for readability, just helping to avoid mistakes. Once you're confident it's written correctly, it actually adds more noise than it does to make things readable.

Which is one of the reasons this is a controversial feature. It the kind of thing that only helps when you're doing things wrong to begin with. (Or when you're using a dynamic language with no meaningful type checking.)

5 Likes

Yes, the shorthand in the "Future possibilities" section would help here:

foo(.argument = argument)
// could be abbreviated to
foo(.argument)

Maybe I should include it in the RFC?

1 Like

I just found an alternative to named arguments (admitted an “alternative” that’s not 100% serious and using unstable features) over here. It would make the hour_angle_from_elevation example look like this:

named_args!( // <- more realistically using a procedural macro

// Calculates the hour angle (in radians) at the location for the given angular elevation.
// - lat: Latitude of location in degrees
// - decl: Declination in radians
// - elev: Angular elevation angle in radians
fn hour_angle_from_elevation(lat: f64, decl: f64, elev: f64) -> f64 {
    let term: f64 = (elev.abs().cos() - lat.to_radians().sin() * decl.sin())
        / (lat.to_radians().cos() * decl.cos());
    let omega: f64 = term.acos();

    omega.copysign(-elev)
});

fn main() {
    let elev = 2.0;
    let decl = 1.0;
    let foo1 = hour_angle_from_elevation{lat: 0.0, elev, decl}();
    let foo2 = hour_angle_from_elevation(0.0, 1.0, 2.0);
    assert_eq!(foo1,foo2);
}

(playground)

3 Likes

Awesome! I love/hate it!

Downside is it doesn't compose. You can't do:

foo.bar().baz{ a:1, b: 2}().bunk();
1 Like

As a previous Swift developer, named arguments in Rust is a great thing for the language in my opinion. Named arguments also will allow the language to enable features not yet easily possible before. I do think maybe part of this proposal should include named arguments in tuples as well, as in Swift the function params are considered of tuple type and I think Rust should follow this closely. Again, super excited about the possibility of named args and hopefully tuples get some love too!

4 Likes

Swift used to consider function params to be of tuple type in Swift 1(->2?), but it was dropped because the inconsistencies caused a buildup of special cases,

eg: func foo(x: Int) exists, but (x: Int) existing is a bug, func foo(_: inout Int) exists, but (inout Int) does not, etc.

It might be possible for Rust to consider function params to be of tuple type, but it would not be following Swift in doing so.

3 Likes

deny(warnings) is an explicit optin to breakage on compiler/clippy updates. There are no back-compat issues with adding new on-by-default warnings.

2 Likes

I'd like to add one more question to that (excellent) list: how should named arguments interact with type checking dynamic calls? Should argument names be part of the type of a function? If an API requires a callback of type FnOnce(i32) -> i32, can I pass a function with named arguments, and does this circumvent argument name checking/enforcement?

13 Likes

Right. I could maybe add another category "type system", with the questions of closures, trait methods, function overloading, and conversion.

I could also add a paragraph about optional arguments in the "language direction" category.

Food for thought.

3 Likes

I thought maybe .= argument as a shorthand to keep the =. But Rust already has a shorthand for structs, so this circles back to using anonymous-struct-like syntax:

hour_angle_from_elevation({lat: 0.0, elev, decl})
2 Likes

No. named arguments are just syntactic sugar to make function calls more readable. They don't affect type inference.

They are not part of the type. This means, you can pass a function with a named argument to a impl FnOnce(i32) -> i32, it's the same as with fn pointers:

fn named(.arg: i32) -> i32 { arg }

fn higher_order(f: impl FnOnce(i32) -> i32) {
    f(42);
}
higher_order(named);
higher_order(|n| named(.arg = n));

let fp: fn(i32) -> i32 = named;
higher_order(fp);

Yes, but I don't see how argument name checking would be beneficial in this case.

1 Like