[Pre-RFC] named arguments

Ada has named arguments ( https://en.wikibooks.org/wiki/Ada_Programming/Subprograms#Named_parameters )

If ten years from now you want to use Rust to write a control system for a large aeroplane, having a less bug-prone language helps. Taking a look at Ada could help.

1 Like

But isnā€™t it usually obvious for compiler what type a given token has? I.e. compiler should distinguish between x: i32 and x: 10 because in the first case the i32 is a type name, and in the second case 10 is a value, so itā€™s not ambiguous, isnā€™t it?

I would argue that features that add complexity have a bigger cost in languages like Rust than in Python or Ruby because Rust is a systems language, which people use to write software that is used longer and is modified less than Python or Ruby. People spend larger portion of the time thinking and designing and smaller portion of the time typing comparing to Python. Code is read and understood more often in system/infrastructure software than in script or a high-level application. Complexity is a bigger concern in these cases. Thus Rust should add fewer features than Python or Ruby do given features with roughly the same cognitive cost and other benefits.

1 Like

Right, so you should favour having language features that improve the reading of the code, even if they cost a little elsewhere. Named arguments is a feature that improves the reading of the code.

No. More syntax means more energy spent in trying to determine which specific syntax is used here.

1 Like

Take a look at a line of code that uses named arguments, how much energy do you need to see if your arguments are named or not?

send_onions(amount: n1, size: s1)

I think you are overstating your point...

Iā€™m not sure, but I think he means that the more ways you have at your disposal to design your API, the more time you will spend deciding on the design.

I think this is true, but as he pointed out, Rust code is supposedly going to be read more often than it is going to be written. For that reason I think that features that add a little more explicitness when it is desired (without forcing it all over the place) are worth the little overhead they bring when designing the API. I can understand that it is not everyoneā€™s opinion though.

I also understand the argument about multiple small features that would end up adding more complexity than the sum of their individual complexity. But this is true for all alternatives proposed thus far (except doing nothing), especially the ones that would require some syntax sugar.

1 Like

interestingly, I'm against this feature completely and I come from a C++ & Java background :slight_smile:

I think this is a question of design trade-offs:

  1. This is indeed a very popular feature in scripting languages which fits with their model of rapid development and less effort on initial design. It also fits scripting languages because they use dynamic typing and therefore named arguments in such a context have more benefits and less visible costs.

  2. C# is an outlier in such discussions because it suffers from acute featureritis - It has basically every possible feature under the sun. I currently work at a C# shop and have to fight with several very stupid anti-features of C# that people claim "makes writing code easier/quicker". For example, static classes make code near impossible to test and regions remove the need to better organize the code (Hey, Let's just put everything in one huge file with tens of thousands LOC and just use regions because we are lazy to use proper logical (namespaces) & physical (file system) organization (end of after work day rant))

  3. In a statically typed lower-lever language such as Rust it is clear that the benefits are fewer and the costs (however marginal people claim) are more noticeable.

Given that Rust has a rich type system that already allows to distinguish parameters the added value becomes marginal at best. and the cost of "there's more than one way to do this" becomes much more noticeable, at least for me.

Honestly, in the vast majority of cases, based on my personal experience & background, I'd consider named arguments a code-smell in the design where the developer didn't consider providing stronger typing for the arguments and/or just has too many parameters which should have been better organized.

4 Likes

Ada programmers seem to like this feature.

i32 is also a valid expression if you had a variable with that name, so yes it is ambiguous. (Any ident token is valid as both an expression and a type)

1 Like

I think this is an important distinction that should be taken into account whenever someone familiar with dynamically typed languages are determining the benefit/cost of named arguments to a statically typed language.

If I were going to do that, named arguments would help way less than using explicit types for everything and using Rust's type system to it's full potential.

Combining the dynamic/static typing perspective with the perspective of writing a control system, the trade-offs become more obvious. If you're going to be writing safety critical software, arguing against using newtypes or the builder pattern on the basis of how much boilerplate is required is almost pointless, since the cost of doing that (and getting it right) is well worth the benefit. It's also not obvious that named parameters would be any better at making the code 'safer'. The scenarios in which the effort isn't worth it is with 'throwaway' code and that's not Rust's 'niche' anyways.

The primary use case for named parameters in Rust is when two parameters have the same type. If each parameter is of different type, it provides only subjective benefit that can be changed by using improved argument names. And I have yet to see an example where a function with two parameters of the same type wouldn't be improved by having those parameters have different type or some other refactoring that results in parameters with distinct types.

2 Likes

Arguably an example of the Blub paradox.

(Another relevant old Lisp argument: design patterns are missing language features, as applied to the builder pattern. That however is only relevant to a subset of calls - those with enough arguments that the builder pattern is currently justified - and arguably the language feature(s) in question already exist, in the form of keyed struct literals and functional record update. I honestly don't understand why people write horribly verbose builders, instead of taking a struct intended to be passed as a literal ending with , ..Default::default(), which isn't much longer at the call site. Maybe a bit of sugar would help.)

1 Like

Because builders are extensible in semver compatible upgrades. Technically, the syntax Ty { field: val, .. Ty::default() } will work even if new fields are added, but it still requires making the fields public, which means that syntax isn't specifically mandatory.

2 Likes

Yeah, thatā€™s one issue. I think the way FRU is desugared was a mistake: given struct S { f: i32, g: i32 } then S { f: 0, ..old } means S { f: 0, g: old.g } which forbids having inaccessible fields among the ones not specified, but it could have meant (let mut tmp = old; tmp.f = 0; tmp) which wouldnā€™t, and would work the same in most cases - but not all. I wonder if thereā€™s a backwards-compatible way to change it. (I proposed something several months ago, but in retrospect it was overcomplicated.)

In practice, though, thereā€™s not much downside to pub _dont_use_this: ()ā€¦

1 Like

Somewhat agreed. We do something similar for the dual of this problem with std::io::ErrorKind. I use the same trick in a few of my crates too. However, std has the advantage of declaring the __Nonexhaustive variant as unstable, which means new variants are guaranteed not to be breaking changes.

I havenā€™t read every proposal here (there are alot) but I donā€™t see this one so I thought I would throw it out there.

To me (and a lot of other people) the biggest benefit of named arguments is default arguments. It would be great if this could be easily accomplishd in a struct. ā€œBut that would require you to import the structā€ā€¦ maybe not.

What if you could write annonymous structs like so:

fn foo(a: u8, b: u8, defaults: {
    bar: u8,
    baz: u8,
    bam: u64,
    zing: f64,
}) { 
    // do foo
}

and then the user could call it with

foo(1, 2, {bar: 10, bam: 12, ...}) // `...` means "rest are default"
// OR
foo(1, 2, {...})  // use only defaults

This way you never have to actually define the ā€œdefaultsā€ struct ā€“ it is defined implicitly. Also note you could use a defined struct for defaults and the user could call it the same way (without needing to import it).

This is also a way of implementing ā€œnamed-onlyā€ arguments, which I prefer for the cases where you want default arguments.

Edit1: this would have the advantage (or disadvantage) of allowing you to define and initialize struts without naming them ā€“ which may be good or bad. It could get you a python ā€œdict-likeā€ experience that is still type-checked.

Edit2: I would think this approach would use rustā€™s type inference ability, which allows you to say let x = foo() where foo() returns type Foo without needing to import type Foo.

Edit3: to have truly default arguments, we would have to resolve how to have default args for structs ā€“ but that is a giant todo anyway, and could use simple syntax.

Iā€™ve come to the conclusion that the best way to convince people to agree to named arguments is to first discuss anonymous structs as a feature. There are already anonymous tuple structsā€¦ which are tuples, so it would make sense if anonymous regular structs were in the language. Otherwise itā€™s a surprising omission.

This would probably placate the people who want named arguments since you could easily pass anonymous structs into functions.

3 Likes

@iopq and declare them as well! You are right, it is a surprising omissionā€¦ is there an RFC for annonymous structs? I canā€™t find one.

If additionally struct fields get default values, then perhaps most issues with named arguments are handled without too much burden on other parts of the language.

Then you could write:

fn foo(opts: { x: i32, y: i32 = 3 }) { ...}

foo({ x: 1 })