[Pre-RFC] named arguments

Is an ascription syntax like "u32(x)" acceptable as replacement for "x : u32"?

But how much (and how often) do we need named arguments and type ascription in average Rust code? Aren't named arguments going to encourage functions with a longer list of arguments (instead of encouraging the usage of arguments grouped in structs and tuples to reduce their number)?

To the first part, it seems like that's rather difficult to say given we don't have either in stable at present. I can say that I'd very much like to have named arguments in particular (type ascription is fine, though I don't want it as much as I want named arguments).

To the second, that certainly hasn't been my experience in Python; it's natural to refactor to a class or dict there if your argument lists start getting long. This does raise an important ergonomics point when comparing to Swift, on which see below.

I strongly agree with the underlying sentiment here from a user (rather than implementor) perspective. The various alternative options proposed above to work around the type ascription problem are, as even those who proposed them noted, ugly. The internal and external parameter names in Swift (and ObjC, thanks to common Smalltalk heritage) took me a while to get used to, but I also like them. (_ x is indeed the correct shorthand for them.)

But the type ascription syntax also seems like an even more natural fit with the overall language designā€”while param_name: value is initially attractive, I do think param_name: type for ascription is even more so (even as someone who's far likelier so far to want a named parameter than type ascription).


To the ergonomics point I mentioned above: Swift's approach to named parameters (unlike Python's) is such that once you have keyword/named arguments, you have to supply them in most cases; otherwise the API developer has to explicitly do func foo(_ bar: Int, _ baz: String) there.

That specific approach would (fairly obviously) not be backwards compatible here, and it's also annoying. It makes calling functions incredibly long and verboseā€”far more so than similar APIs in Rust, which is a fairly unusual outcome on comparing the two for brevity.


From a usability perspective (again, I have no idea how feasible this is from the implementation side), what I'd prefer to see is:

fn increment(initial: i32, by amount: i32) -> i32 {
    initial + amount
}

increment(32, by 10);  // 42

With a case where you had type ascription, that'd look something like:

let amount = 10i32;
increment(32, by amount: i32)

Is it possible to evaluate that call signature unambiguously in the grammar?

(Again, I know almost nothing on the implementation side, this is all just an enormous :question: for me coupled with what I would like as a user.)


Then I'd assume it would play out something like this:

  • named parameters must follow any non-named parameters (both Python and Swift enforce this)

  • named parameters may be supplied without the name (a la Python; Swift doesn't allow this, but I think that's both problematic in Swift and a problem for Rust backwards compatibility)

  • named parameters are defined simply by including the external parameter name. If there is only one name, it's a positional parameter; if there are two names, the first is the named parameter name.

    (Maybe there's a way to avoid repetition if you want them both to be the same so you don't end up with fn foo(bar bar: i32) in that case, but I can't come up with anything immediately.)

:question: :grey_question: :question:

9 Likes

Yes, in a way the ascription syntax is the non fitting one, considering that otherwise only on the definition side a type is following the colon.

Playing a bit around for an other ascription syntax:

foo(<x: i32>)

foo(x@i32)

foo(i32(x))

foo(x is i32)

I think this proposal, if accepted, would make Rust unnecessarily complex. I dislike complex languages e.g. C++ and Scala since the return on investment of learning and using all those features is not even high enough to be measurable, not to mention other issues with complex languages like project-based dialects using only (mutually incompatible) subsets of the language, thereby fracturing the ecosystem. This happens, if only because being a veteran of 1 project means less and less in terms of being able to just pick up another project in the same language as the language itself grows more complex.

Ok, so what do I propose instead then? The same effect (without loss of readability/maintainability) as named arguments can be achieved by method chaining. If we assume struct Point { x0: u8, x1: u8, x2: u8} to be defined, rather than writing e.g. let p = new_point(x0: 0, x1: 10, x2: 100); you can, at this moment, write something like

let = p Point::new()
    .x0(0)
    .x1(10)
    .x2(100);

This also scales nicely in the number of ā€œargumentsā€, arguably better than named arguments. To see this, consider a 100-dimensional version of Point. What would happen? In order to keep things readable, at some point youā€™d need to start v-aligning the parameters to the new_point fn, similarly to what method chaining already achieves.

In light of the above itā€™s hard for me to see the appeal of named arguments for Rust, given the complexity cost.

2 Likes

I would say this syntax is objectively worse than with kwargs. Not everything is a struct instantiation. To quote a previous example:

// might have headers etc as optional options
http.get("https://www.rust-lang.org/en-US/", timeout: None);

Using a builder pattern would mean changing the API to be quite ugly imo:

let req = HttpRequest::new("https://www.rust-lang.org/en-US/")
   .method(Method::GET)
   .timeout(None)
   .exec();

Another disadvantage of the builder pattern is that you need to implement a fn for each field which can mean lots of code for not much reason.

I would say named arguments (and default args) are one of the main reasons Python libraries like requests or even pandas are so elegant. Making something like http://pandas.pydata.org/pandas-docs/stable/generated/pandas.read_csv.html simple for both library author and users is quite impressive.

I would say that named arguments really shine when combined with default args though.

3 Likes

This reminds me of an older idea of using _ to infer type names where they're unambiguous. That would look like foo(posarg, _ { namedarg: ... }). You'd still have to define the parameter structs yourself of course. The upside is that this construct would work anywhere, not just for named function arguments.

1 Like

No the fact that you don't like whereas others may have other opinions on it, is the very definition of subjective :slight_smile: It's true that .param_name(value) is not as syntactically beautiful as param_name: value. That said the difference is minimal since param_name and value dominate visually either way, and in the same order too.

That aside, yeah there's some syntactic overhead, if all implemented manually. But making something a struct, providing an impl and generating getters and setters is something that can be automated and abstracted with regular macro_rules! macros.

Basically it comes down to: you want simpler syntax for something. In most other languages it makes sense to extend the parser in such a scenario, and that includes Python for the purposes of this discussion. But Rust is different. A regular Rustacean has much more power at his or her fingertips than someone writing Java, C or Python. Look at this gist, here I have written some rough macros that basically abstract away the boilerplate you mention. I encourage you to post the code in the gist into the playground to see it in action.

Note that this only works for methods, not for free standing functions, and it requires a struct for ā€œbook-keepingā€. Itā€™s a very useful pattern, and quite elegant too. But stating that it is a better fit for all situations is a little bit of a stretch IMHO.

Also, I know a lot of people will not agree with this, but macros feel kind of ā€œdirtyā€ to me. They are not elegant, errors are awful and their template syntax is incomprehensible to the untrained eye. Reaching for macros to ā€œfill in the gapsā€ in the language is not the approach I would like to see taken.

5 Likes

Named arguments in Rust increase the complexity of the language. So before adding a feature we need to think well if it pulls its weight.

On the other hand, I think the largest part of the "complexity" of a language comes from features that have corner cases and unexpected, invisible, unintuitive or unsafe interactions with other language features.

When you have features that fit together well, have no weird interactions with other language features, are safe and handy to use, and have a reasonably intuitive syntax, you learn them quickly even if your language has lot of them.

3 Likes

Disclaimer : Iā€™m do not own any degree in pure CS, Iā€™m working in machine learning field hence I do a lot of programming/prototyping for ā€œdata scienceā€ ā€œbig dataā€ whatever they call it nowadays. Iā€™m looking into Rust because of the low level/high level combo it offers in contrast to say python/jvm languages (scala mostly).

Anyhow, Iā€™d would love to see machine learning libraries implemented in rust (yes Iā€™m aware of rusty-machine), and IMHO the lack of named arguments is kind of a deal breaker for productivity. Indeed consider a classic model from scikit learn in python :

sklearn.ensemble.RandomForestClassifier(n_estimators=10, criterion='gini', max_depth=None, min_samples_split=2, min_samples_leaf=1, min_weight_fraction_leaf=0.0, max_features='auto', max_leaf_nodes=None, bootstrap=True, oob_score=False, n_jobs=1, random_state=None, verbose=0, warm_start=False, class_weight=None)

I often need to play with these parameters (multiple trains with different parameters), and I really donā€™t want to spend time on figuring out which parameter stands for which one in the mathematical model. I think the following is pretty hard to read/maintain/play with etc.

sklearn.ensemble.RandomForestClassifier(10, 'gini', None, 2, 1, 0.0, 'auto', None, True, False, 1, None, 0, False, None)

And I really dislike the builder pattern, because I would basically need to build one struct for each model (cause they all have different parameters), which is kind of verbose and I think unnecessary.

Consider this as a (probably biaised) random user thought, but I really think itā€™s a good feature for big data/machine learning/data science stuff, which are as of now mostly based on python (+ CPython extensions) or JVM, and could really benefit from a language like Rust. And according to what I know about Rust I donā€™t feel that feature as ā€œadditional complexityā€ on the path to learning Rust.

2 Likes

That Python code is ugly even if you have named arguments. You can't design features of language Y because language X has a library with an ugly API. Can't you invent an alternative better API for that Python code?

Iā€™m not a maintainer of the said code (just user), but be aware that the library (scikit learn) is now considered as industry standard stuff. Iā€™m not against alternative design at all, I guess Iā€™m just not able to figure out one. Iā€™m just saying that between nothing and named arguments, Iā€™d rather use named ones.

EDIT : I would also say that Iā€™m not claiming that the library has a beautiful API and Rust should be able to do the same : Iā€™m saying that mathematical models with a lot of (complex) parameters comes up on a daily basis and thatā€™s something you (at least I) consider when I implement something in a given language.

2 Likes

Indeed, it requires a struct. As a minor detail, note that if it's just 1 parameter the whole thing would simplify to a newtype without any runtime overhead.

I'm not necessarily claiming it is a better fit for all situations; that would be dogma, something to be avoided at all costs when programming (pun intended, but the point is still valid). What I am claiming is that once you want something like named parameters, you are already dealing with quite complex information; complex enough to want to label each individual piece, and this is done primarily for documentation purposes since the compiler/interpreter (depending on the exact language) won't care either way. This observation holds regardless of the exact form e.g. astruct+impl, a fn etc.

I suspect that macros feeling dirty is an example of the Blub paradox at work, though the only one who can say that with any certainty is you.

  • First off, elegance: Conceptually template-based macros are very elegant. What makes the Rust implementation more involved is the "typing" of each kind of information e.g. ty, expr, ident etc. That indeed comes at a cost to clarity, in favor of having the compiler do more correctness checking. But to focus on the impl is missing the point: it would be simple to create a new crate, write the macro's, and upload the whole thing to crates.io. After that it's no more complex for a programmer using the crate than using the vec! macro. Furthermore, it's not like a programmer would necessarily know what a named parameter implementation would look like in rustc, so the ability to white-box inspect it is actually enhanced with macros.

  • Errors are awful: Agreed, macro error reporting needs some love. Specifically, I always only see 1 output line referring to the call site, and all the other barf usually is about macro impl details I don't care about, even in the context of fixing a bug. I suspect it's one reason the core Rust developers want to overhaul the macro system. That said, after reading that one line, usually I know exactly where to go so it's more a "Seeing the forest for the trees" issue than "the information isn't there".

  • Macro syntax: you find it incomprehensible. Ok that's possible. For me, given what it expresses, it is extremely intuitive although a bit noisy. I picked up the syntax in less than 5 minutes and have no trouble manipulating it, but I did need to look it up a couple of times for niche use cases. That's the thing about intuitiveness though: it's not really an objective measure when measured on an individual. The only way I know to turn it into some kind of objective measure is to do it statistically i.e. sampling the entire Rustacean community to find out what the majority finds intuitive. But again, as a macro user you would not deal with the impl any more than you deal with the vec! and println! implementations right now.

Finally: reaching for macros is exactly what should happen, when they fit the use case. That is the entire point of macros (and compiler plugins), so that arbitrary rustaceans can build new control structures and even extend the syntax without rustc needing an update.

See one of my posts above referring to gist, defining the necessary struct+impl does not need to be very verbose, much less than would be the case in Python. In fact it can be a single call, with roughly 1 line per param. If it's the defining you don't like due to the boilerplate (as opposed to the mere existence of such structs) then the macro-based solution will work equally well for you in Rust.

Thatā€™s pretty subjective, with a little bit of formatting it can look as good as the builder pattern (or as bad, depending on your opinion). People seem to have diverging opinions on the ā€œbeautyā€ of both the builder pattern and named parameters, but I donā€™t think itā€™s very productive to argue about thatā€¦


A lot people mention: "added complexity" Can anyone expand on that? Itā€™s an interesting point, but no one goes ever further than just saying it adds complexityā€¦

Syntax discussions aside, I feel like itā€™s one of the less complex features:

  • Implementation would (could?) be relatively straightforward. The compiler can desugar named arguments to their positional form early in the compilation process. There would be no type system changes, etc.

    One question raised was how it would interact with closuresā€¦?

  • This feature is isolated to functions, itā€™s not going to bleed all over the place causing trouble when we want to add other features.

  • There is precedence in other languages, itā€™s not a totally new concept people have to learn.

  • There are just two things a beginner has to learn: How to declare named arguments and How to use them. Both are trivial and they donā€™t even have to be learned immediately when functions are introduced if named arguments are optional (meaning that their positional form can be used)

Iā€™m interested to hear what people mean when they state that it adds complexity!


Have people seen the last alternative I have added, proposed by @flying_sheep ? link

Itā€™s an interesting alternative and I think it should be discussed before moving forward with an actual RFC of any sort. What are peoples opinion about that proposal?

5 Likes

Indeed. At the moment there are 3 kinds of complexity that would be impacted by this change:

  1. For parsing. I imagine the added complexity for this is not trivial, but overseeable.
  2. For analysis. Depending on how much the parse tree (a datastructure, and the result of parsing) can be desugared to a regular fn call, there may be minimal impact here.
  3. The human complexity of a language. The way I have been using it corresponds roughly to how many features in this case Rust has. The less individual features, the more simple the language. A few examples of features are named arguments, first class functions, traits/interfaces, operator overloading etc. It depends mainly on how they are implemented. Another thing to keep in mind is that some features come not as explicitly programmed features, but as the result of composition of 2 or more features of a language. For example, closures are often the result of the composition of, at the very least: 1. Being able to, in a function, refer to variables defined in a syntactic parent scope of that function i.e. referring to free variables; 2. Syntactically being able to define functions i.e. lambda/anonymous function support; 3. A mechanism keeping track of those free variables so that they are not deleted while they may be used by the closure.

Point 3 matters because humans, and therefore programming languages, have what's known as a complexity budget: There's only so many abstractions you can manipulate at any one time. When there are too many options to choose from, humans don't exactly choose optimally, we lose oversight*. Aside from that, I don't think it's controversial at this point that in general composition is a good way to build abstractions, rather than hand-hacking in solutions. Features in programming languages are no different. Composition is quickly parsed by humans when the language is set up for it, whereas a custom feature is yet another thing to remember. It's one of the reasons C has lasted as long as it has: for all its flaws, it is a simple language, in the sense that you can keep all its features in your head at the same time. That makes it easy to reason about**, and pretty much everyone who writes C can talk about almost any feature. Compare that to C++, where the feature landscape looks Lovecraftian, to say the least. Simple example of how C++ is not simple: there are now at least 4 different ways to allocate memory, and the answer to the question "how do I properly (de)allocate memory?" has changed rather much over the years. So - if I may pick on C++ just a moment longer - what a lot of projects written in C++ do to make things simple and sane again, is to make some rules about which features are to be used and which ones are not. This results in what I call dialects***, with the point I made above about ecosystem fracturing following from that.

*Where exactly the boundaries lies may vary between individuals, yet it's existence is so far pretty universal. ** Of course, C is a notoriously difficult language to truly master given its manual memory management sematics. ***It seems appropriate enough to call them dialects, anyway

This is interesting, I like the idea of an opt-in trait for (a) named last parameter(s). I'm just not sure if the compiler mechanics as they are allow for a derived trait to influence parser behavior.

1 Like

It's not ideal, but I it also seems like it would be quite uncommon. The only time it would be necessary to add parenthesis would be when explicitly coercing a variable as an argument (which seems only necessary when calling a generic function, and coercing any other expression would be unambiguous). I personally have never had occasion to do this, though admittedly I haven't written much low-level unsafe code in Rust, yet.

I think : is definitely the right choice for type ascription, and I wouldn't want to change it.

If having the best syntax for both is possible with only a very-rare corner case for which we can provide a good error message, I think it would be quite unfortunate not to go with that option.

I think this would be ambiguous:

increment(32, by (a + b) / c);

Is this specifying a named argument, or is it calling the function "by"?

2 Likes

Isn't "Type(x)" syntax better?

1 Like

How could that work? That already means ā€œcall the Type constructor with value xā€.

struct Foo(i32, i32);

fn elsewhere(x: i32) -> Foo {
    Foo(x.into()) // where as now: x.into() : Foo
}

Ironically(?), I think C++ might have benefitted from named parameters. As it is, if you want multiple constructors for a class that initialize the object in different ways, you have to rely solely on type and number of parameters to distinguish them, leading to monstrosities like std::try_to_lock_t (an empty struct that certain constructors take as an argument for disambiguation alone). But of course that doesnā€™t apply to Rust since ā€œconstructorsā€ are just functions that return an instance and can be named.

IMO, youā€™re not wrong that named parameters would add significant human complexity to Rust, mainly because they add a degree of freedom for people to diverge from idiomatic Rust: you might see codebases that mimic Swift and annotate as many function calls as possible with named arguments.

However, Python has let you use keywords for any and all arguments (except for builtin functions) since 1996, and most code Iā€™ve seen is pretty consistent with how it uses them: only for functions that take a lot of arguments, or if the meaning of the arguments is significantly nonobvious.

1 Like