[Pre-RFC] named arguments

Hm, your approach may be indeed a better solution considering Rust's spirit. But note, that I propose to issue only warnings, not compilation errors, so you will not need #[allow_positional] for backwards compatibility. Of course it could be a compilation error instead, but I don't think it's worth the churn.

A huge :+1: for this proposal, although as mentioned already I would prefer if the arguments have to be named by default, and with an #[allow_unnamed_args].

1 Like

Why allow default arguments at all? Why wouldn't the reasoning to not implement default arguments not apply here?

The post describes the direction in which I would like for Rust to move and this includes default argument values (a.k.a optional arguments). Several people even think that named arguments don't have enough motivation on their own without default arguments (though I disagree with this opinion), for example:

In the real RFC I think default arguments should go to "possible future extensions" and probably proposed separately in its own RFC later.

Can you please re-iterate arguments against default arguments for informational purposes? I am sure default argument proposals were discussed extensively, but I haven't read those discussions and AFAIK those arguments haven't been raised in this thread.

2 Likes

I thought that it was an ABI thing for why rust didn't have default arguments, but I can't find a reference to it, nor can I find any lang team members who were actually against it in general, so I may have been mistaken. It looks like default arguments were just never implemented:

Optional arguments was cited as a major reason why the other major default argument proposal wasn't accepted (default optional arguments would solve the issues and do more, like what you are suggesting).

Relatedly, I wonder if instead of adding named arguments and default arguments as-is, it would be possible to extend structural records themselves with a Default impl that could allow code like

foo({ arg1: bar, ..Default::default() })

which would then solve both problems with structural records and save us of the problems "proper" named arguments usually cause.

7 Likes

My take on this using default field values + structural records is that we can have:

type Config = { height: u32 = 1080, width: u32 = 1920 };

fn open_window(config: Config) {
    // logic...
}

open_window({ height: 900, .. });

(..Default::default() would also work with just structural records)

16 Likes

My take on this using default field values + structural records is that we can have:

I like this direction a lot.

I still would like to see type inference for struct literals. I would love being able to replace the 'type Config' in your example with just a 'struct Config' and still being able to create a 'Config' from just '{ height: 900, .. }'.

I think currently type ascription makes this syntax ambiguous, but have this kind of type inference for structs would be IMHO a even more general solution.

Meh. I'd rather have a form of struct name elision, so you could still write

open_window({ height: 900, .. });

but Config would just be a regular struct:

struct Config { height: u32 = 1080, width: u32 = 1920 }
6 Likes

There's no technical reason why you cannot have both. :slight_smile:

6 Likes

I can't emphasize enough of how much I like this idea and approach.

3 Likes

In a Unity / C# project at my previous company, I recommended we use named arguments for any literal, especially Boolean. With point free style, you can get away with unnamed arguments in low arity functions, but in Unity, or any complex system, I find it more difficult. After adding named arguments, we could easily see what all the True and 1.0s were for. In C#, they are optional, so it’s up to the discretion of the author / reviewer. I’m surprised there has been no mention of C# here, just some Python.

1 Like

To be fair to Rust, it's unidiomatic to use true and false outside of FFI. You're supposed to define enums instead, since their variants are self-describing, most argument transposition errors can get caught at compile time, and they're easily extensible if you decide down the road that you need a third or fourth variant.

3 Likes

I disagree. Bools are quite common and often unavoidable.

5 Likes

Yet using enums is a real way to have many of the named arguments benefits for booleans. There's no guarantee that people will use named arguments. Both the named arguments and the enum booleans are opt-in solutions for unobvious booleans meanings.

I think that people don't use enums for bools because it's not a common in other (common) languages, whereas named arguments are. So I do think named argument would have some impact on boolean soup, but it's not the only way to tackle the problem. I do wish the book would mention custom enums as a self documenting alternative to booleans.

3 Likes

I personally use the enum MyCondition { Yes, No } approach (with the variants named like so) when adding such code in rustc. I find the practice quite robust (more than named arguments would provide for) and self-documenting. It also provides a good place to leave doc-comments when I feel that additional information is necessary.

4 Likes

I like to go a step further and pick variant names that remain locally self-describing if use is used to remove the need to include the enum name. (eg. Something like Recurse instead of Yes.)

Makes sense. I tend to avoid useing the variants unless it's a lot of uses and it's e.g. very locally used like when matching on an enum with many variants (e.g. ExprKind might be a candidate). Typically I would write if let RecoverComma::Yes = recover { ... } or some such. (You can also write Self::MyVariant if its in an impl...)

1 Like

It works, but if you want to compute derived expressions (e.g. createFoo = needFoo && !haveFoo), you end up with annoying conversions between those enums and bools or other enums. It also requires you to write a separate type declaration for each condition, which gives it a cost of sorts: there's always a temptation to skip it and just pass a bool. It's subjective, but personally I see the approach as inelegant – a reasonable workaround in the absence of named arguments, but inferior to them.

6 Likes

The same argument could be made against Rust's decision to require explicit type conversions between different width integer and floating point types.