[Pre-RFC] named arguments

enums don’t play well with C FFI either, because C doesn’t have tagged unions

I still think this would be a useful addition to the language. Although, this discussion has shown me that even though the majority wants named arguments in some form or the other, everyone has it’s own take on how it should look like. In the last couple of weeks I have been hesitating to write the actual RFC and what I should modify from the original proposal. I think I’m just afraid to re-trigger the same discussions as in this thread without really converging to a good solution.

If you want to take over the lead from here and go through with the actual RFC I have no problems with that. :slight_smile:

For what it’s worth, we discussed the named arguments issue, together with optional arguments, in a recent lang team meeting. The lang team is definitely open to considering something in this space, but there’s not pre-existing consensus about the best way to do this (e.g. via a direct feature vs “encoding” through anonymous structs etc).

If someone does want to pick up the mantle on an RFC, the lang team would be happy to provide a “mentor” for the process, including providing feedback on drafts. Let’s figure out who wants to push forward on it, and I’ll hook them up with someone from the team.

EDIT: I should also add that RFC collaborations are fine, and are something we’d like to encourage more of! (We have some process changes in mind here that should eventually help). The important thing is to have at least one person committed to pushing through the process.

3 Likes

I would be interested in collaborating on the RFC if it is towards the anonymous structs approach. As I mentioned in my comment from a few weeks ago, I believe there is some very elegant unifying principles by adopting that approach.

@Azerupi’s original proposal https://github.com/rust-lang/rfcs/issues/323#issuecomment-199067671 seems the right way to go (and use the same restrictions as Python). Anonymous structs would indeed be useful, but don’t solve this problem and should be a separate discussion.

To avoid more bikeshedding, we should be willing to push this feature without named-only support.

1 Like

FYI, I posted this in a users thread already, but I wrote a compiler plugin to support named arguments in today's Rust:

After originally posting it I realized I missed the obvious and forgot to support multi-module crates properly, but I'll fix it when I have the chance. It's not just a toy; the goal is for this to become stable and usable in real code, if only as a way to experiment with the feature.

5 Likes

Anonymous structs are interesting, but there is absolutely no reason this feature should exclude functions using calling conventions beyond Rust, and that’s what that effectively does.

Lots of C functions have many parameters. You can find lots of examples of this buried in the Windows API, for example. If you did anonymous structs, calling such functions via keyword arguments becomes nonfree because you need a wrapper that turns the anonymous struct back into positional args. or you have to special case it in the language. But either is less than ideal. Then we get into weirder stuff like the MSVC calling conventions.

There’s also something to be said for potentially optimizing Rust code by generating specialized functions for various parameter combinations. I don’t think this should happen initially, nor do I plan to propose it anytime soon. But if you have a builder taking 30 parameters and 10 of them are booleans, it may be advantageous to specialize it so that you can statically eliminate branches that can’t be visited. Anonymous structs would let you do this as well, but it is again more advanced.

And finally, simple rewrite rules don’t require extending the type system. I’m not fully against anonymous structs as the solution and agree that they are nice about unification, but this seems like a great deal of work with disadvantages vs. very little work with disadvantages. And the proposal here doesn’t preclude adding it later.

1 Like

I would be willing to be at least a part of it, but need to familiarize myself with the full process.

If there’s more than one person working on this, I might not be the best lead. Refactoring trans only shows determination, not experience with navigating the change-the-language waters. If someone else doesn’t want to take the lead, however, I certainly will.

I don’t think this is true, I can imagine that for FFI we could offer guarantees about how anon structs are mapped in to the calling convention as part of the ‘sugar’ for how they can be used.

I’m not convinced that anon structs are the answer, but I’m even less convinced that an ad hoc extension to function call syntax is either. I do think it is important that we consider default arguments too - that seems to be a good part of the motivation here, and even if we leave the details for later, I’d like to be sure of a path forward.

2 Likes

If we start with the ad-hok extension to function syntax and only allow it on nonclosures, we can later integrate it with the type system if we so desire. I.e. syntax could be provided to say “Takes an Fn with 2 arguments named a and b”, or what have you.

The problem I have with actually doing that is that you start tying things really tightly to the names of things in user code. Allowing me to call your function with named arguments is one thing, but calling my callback function with named arguments introduces the problem that nothing makes it obvious that these names are important at the point where I defined the function. Consequently, I don’t think that making it anything more than a lint is really such a good idea: too much spooky action at a distance. And, since it’s now not a hard error, it’s really hard to justify putting it in the type system.

Anon structs being special cased for all the calling conventions that aren’t Rust just lost the big advantage of anon structs: they aren’t general anymore. SO to me that’s sufficient to take them off the table completely, unless someone comes up with a good reason why they shouldn’t be. But there is an additional objection. How do you declare an extern function properly if it’s taking only one struct instead of n arguments? It seems to me that you now have to extend it with extra attributes for the FFI case ala the recent repr(transparent) RFC.

But the thing about the simple extension to function call syntax is that it’s really easy to implement and solves many of the common cases: reduce need for the builder pattern, remember what arguments are at the call site of complex functions, and make it possible to leave out arguments that always have sensible defaults (i.e. Vec3D::new(y = 5) or similar). I think it’s essentially just a pass over whichever IR is most appropriate to change let’s call it a complex function call to a vanilla function call.

But I feel like this has probably all been said, should I put aside the time to read this incredibly enormous thread. I suppose I’ll have to, but who knows when–it’s really quite a lot, and I’d probably need to take notes to synthesize it all.

With the new release of IntelliJ IDEA today (2016.3), JetBrains have actually done named arguments for Java, but only via tooling. Like so: https://www.youtube.com/watch?v=ZfYOddEmaRw

While this isn’t a universal solution (it requires the right tooling, as opposed to writing it out in the code), maybe the language server could be made such that tooling like this can be made really easy to do? That seems like a sensible solution to me at least; we don’t clutter up the language, and those that want can hopefully have easy access to the right tools to get the job done.

4 Likes

Just to toy with some proposed ideas for function call improvements. Using anonymous structs we can simulate structural types:

fn foo({a: &str, b: usize}) { /*...*/ }

Generic over anonymous structs, hat tip to @cramertj’s generics over tuples idea, if combined with proposed trait fields you get something serviceable at definition site, and very nice at call site.


trait Fields {
    let a: usize
    let b: &str
}

fn foo<F: Struct + Fields>(fields: ..F) { /*...*/ }

struct Bar { a: usize, b: &'static str, c: Vec<usize> };
impl Fields for Bar { let a = Self.a; let b = Self.b; }

let bar: Bar = /*...*/;
foo({..bar});
foo(..bar);
foo(a: 3, b: "test");

Maybe .. could be used for sugar for the Struct trait in the type position, e.g.

fn foo<F: ..Fields>(fields: ..F) { /*..*/ }

If Rust gets something like any for quick parametric types:

fn foo(fields: any ..Fields) { /*..*/ }

While this is still a little heavy on the definition side it would allow for traits such as Debug to be added more easily. I can’t figure out a good way to integrate default values in with this. Though between const, trait fields and a little sugar something feels possible.

1 Like

The problem with the struct based solutions is that they add API boilerplate.

Not only defining named arguments is more complicated than necessary, but the user also has to look up the definition for the fields struct to know what arguments the function accepts, instead of them being defined inline.

I’m strongly in favor of inline solutions.

5 Likes

My suggestion is simple: type ascription, when used in function arguments, should always require parentheses. This leaves f(a: b, c: d) free to be used as the named argument syntax.

4 Likes

My suggestion though influenced by swift:

fn suggest(a: i32, b: i32) {  /*..*/  }

This is the same as provided by rust with positional parameters.

fn suggest(_ a: i32, _ b: i32) { /*..*/ }

This is with named parameters with names corresponding to the parameter names. Shorter than using pub.

fn suggest(another a: i32, with b: i32) { /*..*/ }

This one is with named parameters with alternate parameter names for creating builder like syntax.

1 Like

I've read most of the posts here, and I haven't seen 1 single argument for why Builders aren't good enough other than "lots of boilerplate somehow".

What bothers me about that line of thought is that this is Rust we're talking about, complete with macros. And someone had actually already leveraged that to reduce all that code to a derive. The project is located here:

I think I remember a few from the discussion but here’s one: you can have functions without any structs involved so the builder pattern is not even an option there.

1 Like

You can indeed. But my counterargument is that once the fn signature is complex enough to warrant the extra annotations, it is also complex enough to warrant refactoring it to a builder*, even if that builder is an empty struct. Is there even any situation in which that wouldn’t work? If there is one I have yet to encounter it, but I’m open to such use cases.

*Especially with the rust-derive-builder crate

It’s not necessarily complex functions, just that an argument would be the default 90%+ of the time.

Let’s take https://github.com/Keats/jsonwebtoken/blob/master/src/lib.rs#L104 as an example. It is not a complex signature but in the basic case validation will be Validation::default() so my ideal signature would be fn decode<T: DeserializeOwned>(token: &str, key: &[u8], validation = &Validation::default()). I’m not going to create an empty struct for my functions that have a default argument, that’s a UX nightmare.

Regardless, this thread is about named argument so an example specific to named argument without default argument is readibility/maintenant. For example, a method on a struct taking a bool as an argument (or 2). I think it’s fairly uncontroversial that being able to write this made up method self.render_page(&page, with_livereload=true) is more readable than self.render_page(&page, true) as there is no need to go to the definition to see what the bool corresponds to. It could use an enum but that’s overkill when it’s a simple bool.

I personally view the builder pattern as an (ugly) workaround the lack of named/default parameters so I’m probably biased.

Ok so there are a few things: For those kinds of defaults to be always emulated libstd in that I just write multiple constructors with descriptive names. I’m not saying that is necessarily always the way to go, it’s just an alternative that has the advantage of existing today.

Second, I think I see what you’re trying to say, but your example is an antipattern in Rust: rather than use a blind boolean, it is both more readable and more ergonomic (at 0 extra runtime cost relative to the boolean) to use an enum with explicit variants. That eliminates that problem in a very maintainable and self-documenting way, and again has the advantage of existing today.

Here’s the thing: I suspect you view Builders as ugly relative to named params because all the code makes it feel like a “heavy” solution. The thing is, in most cases it’s actually possible to just inline the builder methods and thus have no runtime overhead. And as for the source level: it may look somewhat verbose (though not overly so when both the builder method invocations and the named parameter lists are v-aligned i.e. when comparing them in an apples-to-apples fashion), but it has the advantages of being able to provide impls for traits, something named params will never do.

That is why I’m against named params in Rust: they don’t really offer anything new except syntactic sugar which doesn’t even make the code all that shorter compared to builders (ie very limited upside), while they do raise the syntactic as well as semantic complexity of the language (2 very real downsides).

2 Likes