[Pre-RFC] named arguments

I have come into the Rust world only recently (less than a year ago), but we value your opinions a lot. Thank you for answering.

Indeed, that looks like too much stuff :slight_smile: For me Swift has too much "stuff" (syntax, special cases, etc).

This feature is much closer than named arguments to the top of the list of the features I'd like to try in Rust as gated features. I don't have much actual experience in Ada programming, but seeing how integer overflows detection saves my rump all the time in Rust, and having read plenty of Ada code, I believe a well implemented ranged integral values will be handy in Rust. But this is material for a different thread.

Thanks for the reply! Can I ask for a little more detail on why you find named arguments noisy? And eventually what other costs you consider. I would like to fill in the drawbacks section and eventually find solutions, compromises or even alternatives if possible.

Personally I find that, on the contrary, in certain cases they help with readability. I understand your point about multiple ways to say the same thing, though, I think this is acceptable to "bend the rules" when the features play nice together and have different trade offs making them better suited in certain situations. (I'm saying this in a general sense, not specifically for this proposal)

Also, if I may ask, what are your thoughts on default arguments?

Stupid suggestion, just to get it out there: we can fix the ambiguity by using a prefix. Using :, we get

http.get("https://www.rust-lang.org/en-US/", :timeout None);

Window::new(:title "Title", :x 20, :y 50, :width 500, :height 250);

free_fall(:z0 100.0, :v0 0.0, :g 9.81);

That said, I think everything but the anonymous struct case is going to have unpleasant ramifications for Fn* traits and language consistency.

1 Like

I’m frankly baffled by the resistance to this feature.

It seems to me to be a relatively lean syntax sugar that is fairly easy to comprehend even for novice programmers (even when mixing positional and named arguments). It’s an enabler for having default arguments in many places, and therefore allowing APIs to build up over time and complexity; i.e., on the API user/learner side, making simple things easy for the API user while making complex things possible, while on the API builder/evolver side, allowing progressive additions to API without breaking source compatibility for existing users. These seem like huge benefits for a large population of users, and therefore worth some cost in terms of implementation complexity (though of course it should be straightforward to teach – but I think it can be).

An anonymous struct solution, by contrast, requires adding struct arguments to every possible API where you want to evolve, which means either (a) prematurely adding options struct arguments all over the place just to be on the safe side or (b) breaking API compatibility anyway when you want to evolve the API.

Going by Graydon’s Rust way points, it seems to me that:

  • Named arguments probably won’t introduce unsafety
  • Named arguments are definitely kind to novice users from the dynamic language audience
  • It does not seem like named arguments will inhibit performance in the common case
  • Named arguments have a lot of potential to actually make callsites less chaotic

BTW, I had to look up truculent despite the fact that I consider myself, while not a native speaker, fluent in English, so if this actually makes it into some kind of canon, consider replacing it with something more accessible (or even “kind”).

In general, it feels like I see a lot of resistance to syntax changes in this community, compared to e.g. the Python community; even if the change does not cause breakage per se. While I get that it was necessary to freeze stuff so 1.0 could get out the door, it would seem to me that, as a young language, there are still a lot of benefits to be had from small syntax changes that don’t have big compatibility impacts. The big complexity in today’s Rust seems to reside much more in the type system than in the syntax, yet it feels like people have no qualms about adding more complexity there. The syntax certainly seems like it could bear small amounts of additional complexity, and lots of people have been asking for this one for a long time.

I thought Steve Klabnik’s division (in his Rust history talk) of Rust audience in systems people, functional people, and dynamic people was a very interesting characterization; I’m definitely in the latter camp. One important lesson from that camp seems to be that its developers are used to all kinds of niceties not generally available in systems (and most functional) languages, and I think Rust is attractive exactly because it provides many of these niceties in the systems space.

5 Likes

It shouldn't be that difficult to understand.

Every single new feature has tremendous long-term costs. It has to implemented, then maintained at least until the next breaking version (which might be years away, if ever). It has to be documented. It has to be explained. It has to be considered when designing and writing code, yet another moving part to keep in mind. It's yet another piece of syntax for new users to see and think "what the heck is that?" and then have to try and google syntax (which never works well). It's hundreds of the same Stack Overflow questions over and over again because so many people don't bother to search, or don't know what they're searching for in the first place, or how to word it.

It's yet another grammar construct that can't reasonably be used for something else. It's another thing that doesn't interact with the type system in a clear fashion, possibly leading to even more complicated and hairy type signatures once you get out of the realm of obvious examples and into the edge case minutiae. It's another complication for tools that aim to work with Rust code (which we already have a relative paucity of). It also doesn't really compose with other features; it's just a dead-end corner of the language that does this one thing.

And to top it all off? You don't need it. There is no credible argument you could make that this feature is actually essential for the language. The problems it seeks to solve can already be handled in other, currently implemented ways.

There are already so many languages that will grab every feature they can get their hands on. I'd argue that most languages made in the last decade or so do that. I think it's a great thing that Rust is being developed by people willing to say "You know what? No." People willing to not accept the simple and obvious solution, but to wait for something fundamentally better.

Rust is conservative in a world filled with "implement all the things!" languages. I'm glad for Rust in part because it isn't Python, and isn't C++. It doesn't need to pack on every last feature it could theoretically have. That's how you end up with a bloated, corpulent language where people have to start defining subsets just to stay sane.

To say you're baffled by the resistance suggests you aren't considering how people like me feel about Rust. Rejecting your opponents as insane is a great way to completely sabotage any possibility of reasonable discourse.

You want to know the kicker? I want named arguments.

But, at the same time, I'm thankful that there are people in this community who will pick at every little unsettled detail, force new proposals to go above and beyond to prove their value, and to withstand relentless scrutiny.

Not always happy about it, mind you. I'm still annoyed by an RFC I wrote being rejected (meaning Rust didn't end up avoiding the problem it was written to head off), and I'm really sad my current RFC is probably going to be rejected even though I think it's a good solution to a fairly frustrating problem the language has right now... but if that's what it takes to keep Rust healthy and lean, so be it.

</2¢>

12 Likes

I mostly agree with what you said.

My intention is not to introduce a half backed feature into the language. I want to discuss the ideas and iterate over the proposal until we come to something most people can agree on, or we come to the conclusion that it is just not possible to do so. That's why it's a pre-RFC and not an actual RFC.

I think some people have raised valid concerns but all of them stay relatively superficial and do not go in much details. Most of the discussion was centered around the parsing ambiguity, which is in my opinion not the most important aspect to discuss at this point.

Here are some points raised, which I would like to discuss further:

  • Should named arguments be part of the type? People have stated that named arguments would cause complications with fn and Fn types and in particular with closures. What would be the advantages / disadvantages of making named arguments part of the type vs not part of the type.

  • Added complexity / cost Complexity can have multiple forms, where would named arguments add complexity and how could we minimize it? Is the ergonomic benefit also considered in the "cost evaluation"? A lot of complexity can often originate from weird interaction between features or corner cases, in this regard named arguments seem to be relatively well isolated as a feature. Thoughts?

  • Named arguments are not that useful without default arguments True. The point of this RFC is to get things moving in the direction of default arguments. But since it's a colossal feature that needs a lot of discussion and has some technicalities that need resolving, it seemed like a good idea to split the work into smaller chunks.

  • Alternatives currently exist (e.g. builder pattern, structs, ...) Can we objectively compare all the advantages and drawbacks of the different alternatives? Proposed alternatives are often excellent choices for specific situations but show some short comings in other situations. It's difficult to draw conclusions without having a list of advantages / drawbacks for all alternatives.

Nothing, it would cause a breaking change just like changing the function name or changing the arguments type... But once the author has marked an argument as public he has made a conscious decision about the stability he wants to provide. It's the same meaning as marking a function or a struct as pub that is why using the pub keyword in this context is appealing.

3 Likes

I've noticed this too. If we understand "people" here to mean the people who comment on RFCs (and pre-RFCs and so on) rather than the people in charge of deciding about them, then I think this is largely just due to "the bikeshedding effect": way, way more people feel competent to evaluate and substantively debate syntactic changes than type system changes. So it's not that they necessarily "have no qualms" about adding complication to the type system, just that they trust that the people who have the relevant expertise will do a good job of handling it. And there's nothing wrong with that. People contribute where they can. But I agree that in the aggregate the effect feels somewhat bizarre, where minor syntactic additions tend to get bogged down amid fears of "complexity" while major type system features are sometimes barely commented on.

For what it's worth, my personal opinion about named arguments also tracks roughly with that line of thought: I used Objective-C at work for half a year or so, and in that time, despite the initial weirdness, each argument having its own name at the call site really grew on me. It's really helpful for readability. So all else being equal, I think named arguments are a good thing for a language to have. On the other hand, Rust has already grown up without them and is already a fairly big language, especially at the type level, and I don't feel like it would be worth complicating that any further for mere "niceties" like this. So I think a minimalist formulation which allows top-level fn items and methods to have and be called with named arguments (roughly as I outlined in my previous comment), but doesn't involve any complexity cost to the type system, is something I would be modestly in favor of.

1 Like

To put this into perspective: I think a lot of these design decisions stem from the need for Objective-C compatibility. As I understand it Swift methods have to be able to match to Obj-C methods (messages), which are used like

[Window newWithTitle: "Moin"
                   x: 0
                   y: 0
               width: 500
              height: 400];

where the name (selector) of the method (message) is newWithTitle:x:y:width:height:. You can't reorder or leave out any part of this, it would be a separate method (message). Hence the overloading for a different set of argument labels.

Rust obviously doesn't have these requirements, but I think the list of specific things to look at is good food for thought.

1 Like

I understand all that. By saying I'm baffled I'm signalling that I have a very different perception of cost/benefit of this feature compared to Nick/Graydon (in particular). Also please note that, by saying I'm baffled I'm saying that I don't understand, which is very different from "rejecting your opponents as insane". Obviously noone here is insane, I just disagree vehemently on the perception some people seem to have of this feature.

A grammar construct that doesn't get used for something else seems like a good thing, that's focus. We have to work out how it interacts with the type system, but I haven't heard great arguments why it cannot. Also, I haven't seen many edge case minutiae where it actually goes wrong.

Saying you don't need this, to me, is limiting yourself to a narrow level of what code is. If you look at code as an ecosystem, at evolving libraries, at people learning new APIs and extending the surface of an API that they're familiar with, then I think you do need this. It may not be essential-essential, but I believe it's very important.

I'm not saying Rust should "grab every feature"; this particular one, combined with optional arguments (see also my pre-RFC), seems to be one of the most popular requests. That, by itself, should give everyone some food for thought. Yes, sometimes the customer doesn't know what he/she needs, but I haven't heard any alternative from the opposition that seems credible (I certainly don't think anonymous structs are a viable alternative).

It's this that's the problem. It only helps some callsites some of the time, but it introduces ambiguity in how to write and how to read all callsites everywhere, and makes the grammar and reasoning about arguments strictly more complex, across the language and all libraries.

Insofar as chaos == too much complexity, this adds unnecessary complexity, and that's my objection to it.

On this generalization, I disagree most firmly. The syntactic complexity of the language is already well overbudget.

Many corners of the type system are ignorable by most users, only even appear necessary "when you need them" (to specific library authors, or specific statically-inexpressible things); and often type system work is motivated by a specific safety or speed concern.

Syntactic complexity tends to cost everyone in the language, and the aesthetic / convenience motive is often more subjective.

2 Likes

I’m sorry, but what “ambiguity” are you talking about? Callsites are strictly more useful with named arguments - at worst they are the same as they are right now. Ditto for declaration sites.

I’ve worked with many languages that have named arguments and I don’t recall any problems at all with them. The closest was “this guy should have really used named arguments instead of positional ones”.

I also contend that named arguments present any significant learning barrier. They can be explained in a single paragraph and probably most readers will already have encountered them in some other language.

And realistically, leaving this for individual developers to implement is going to result in a mess of incompatible implementations, each one requiring special attention. Doubly so for macros.

1 Like

I mentioned it above: ambiguity around the meaning of positional (unlabeled) arguments due to argument reordering and caller choice of whether to provide labels. Ambiguity which gets worse if you add default args or vatiadics.

As I said, copying swift in this regard (no reordering, no caller choice of whether to provide or omit labels) makes the feature less complex. It’s still additional design complexity, but less.

In most languages positional arguments can not mix with named arguments, it might be possible to start with positional arguments and then switch to named ones. And named arguments are named, so what’s the purpose of keeping them ordered?

1 Like

For me learning and using Rust syntax was easy compared to learning to handle the borrowck.

As a long-time (~12ys) Objective-C programmer and nowadays (almost) exclusive Swift & Rust programmer I’ve found myself to be more and more annoyed with mandatory(!) named parameters in Swift these days even though I used to be a huge proponent of named parameters in Objective-C.

Why? Because it penalizes properly named variables.

Whenever one names a variable, say, radius and passes it to a function taking an argument radius one ends up with

draw_circle(location: location, radius: radius, color: color)

Ugh. One would almost be better off with badly named variables:

draw_circle(location: l, radius: r, color: c)

in regards to readability and/or a 80 char limit, e.g.

In my experience in many cases the variables passed to a function have the same names as the arguments of said function, resulting in unwanted duplication. It penalizes properly named variables and makes it even convenient to do otherwise.

The main problem here is that swift makes argument names mandatory.


This being said I too would love to have opt-in (at declaration-time, preferably) named arguments.

From a Rust-user’s point of view I would prefer a flavor of the “Struct sugar” alternative with anonymous ad-hoc structs.

Declaration-site:

fn draw_circle({ location: Location, radius: Radius, color: Color }) {
    println!("<location: {}, radius: {}, color: {}>", location, radius, color);
}

Call-site:

draw_circle({ location: location, radius: radius, color: color })

For a function either all its arguments would be named or none would. The naming decision would further more have to be made at declaration-, not call-site.

The general language (from user’s perspective) would thus remain pretty much as it is and named parameters be primarily used for argument-heavy edge-cases.

2 Likes

Another point for anonymous structs might be, that getting default values with them might be lot more straight forward then having default values for function parameters.

I’m not sure I understand you correctly because at first you complain about mandatory named arguments in Swift and then you propose the struct sugar alternative which would impose the exact same thing. This seems a bit paradoxical, is there something I am missing?

In the current proposal, named arguments are never mandatory. The user can always choose to use the positional form. Essentially it is giving just a little bit more freedom to the user to be explicit if he desires so.

Also, people seem to dislike functions with many arguments. Anonymous structs would incite the opposite behavior IMHO, because using an anonymous struct to get one named argument looks a little bit silly (my personal subjective opinion).

2 Likes

I'm not against named parameters. They have their use cases.

Swift however uses opt-out named parameters (at declaration-site via _ label:), which virally infects the entire code-base (Swift's current parameter syntax inconsistencies aside). Having an opt-in struct variant in Rust would allow one to selectively make use of named parameters in edge-cases (example: functions in glium taking several parameters).

Using a well-known and well-encapsulated syntax construct such as struct's { foo: …, bar: … } would imho create some middle-ground. It'd be opt-in (as in non-viral) on a global scope yet all-or-nothing (as in consistent and "simple" [sic!]) for a local call-site's scope.

Indeed. Everything else would be a major breaking change anyway.

So do I.

I see use-cases where named arguments are a clear advantage (compared to properly named variables) to be edge-cases 9/10 times. Pretty much the only times I wanted explicitly named arguments in Swift was for overloaded methods (draw(circle: …), draw(rect: …)). Rust doesn't have those to begin with.

Making named arguments require more effort from the programmer by requiring them to commit to fully-named arguments and add write foo( { bar: … } ) instead of foo( bar: … ) would, imho, make one think twice before forcing call-sites to type named arguments (and for functions with lots of arguments the additional {} as little to no additional weight).

My thoughts exactly.

Also foo( { bar: … } ) would be much more explicit (anonymous struct) and familiar than some mysterious labels showing up in a call-site foo(bar: …). We already know labels from struct syntax.


Now that I think of it Swift (up to 2.x, I think) had something quite similar:

func foo(bar: Int, baz: String, blee: Float) {}

could be called either via

foo(42, baz: "", blee: 12.34) // named arguments

or via

foo((42, baz: "", blee: 12.34)) // tuple with named members

which in a way is equivalent to passing an anonymous struct.

It was removed recently as it interfered with overloading (making func foo(…) both a three-argument as well as a single-argument function). I'd see Rust to require the API designer to choose between either unambiguously.

1 Like

Just to weigh in here, I’d like to point out how Ruby handles named arguments. Not necessarily the direction we should go, but it’s reasonably simple semantically.

  • Arguments are either named or positional, not both
  • Named arguments may be given in any order
  • Named arguments may be given a default. If no default is given, they are mandatory (similarly to positional arguments)
  • Positional arguments must come before named arguments
  • Variadic positional arguments are captured as an array
  • Variadic named arguments are captured as a map

For Rust however, I think by far the biggest question is how named arguments would impact the various Fn traits. I think the simplest answer would be to either go with the original proposal of having the pub qualifier on argument names allowing them to be named, but having them continue to be positional for the purpose of Fn traits – or to go the Swift route of having separate internal and external names, but continuing to have them be considered positional for the purpose of Fn traits. My point being that I feel that any answer which involves anonymous structs or #[derive(Params)] is significantly overcomplicating the issue.

4 Likes

Why not have all functions able to use named arguments by default? After all, all arguments ARE named in the function definition. So fn fuz (x: i32, y: i32) {//...} you should be able to call either as fuz(1, 2) or fuz(x: 1, y: 2).

1 Like