[Pre-RFC] named arguments

Because that would be a major change in behavior. All argument names would suddenly become part of the API, which is probably not something API authors want.

This is why I think a seamless way to “promote” positional-only arguments to named arguments is not such a bad idea.

1 Like

Graydon, while I more or less agree with your position, I feel your argument would be better if it contained specific examples of where the complexity is too expensive. So far, this thread contains examples of good use of named arguments, but no examples of why not.

I mean, if you use the original syntax example:

f: fn();
f2(f: fn());
// makes sense - type annotations
f3(f: f);
// wait what?

Two things: it makes it clear that you are committing to something in your public API, and that the code just reads better. I find that, in the call, you often want to use a verb or other part of speech (e.g., foo.increment(by: 5)), but in the code itself, you want to referent to the value as a noun (e.g., value). It's not terrible to write foo.increment(value: 5), but I think it requires you to shift your perspective a bit to the callee instead of the caller, imo.

Even if you don't want to change the part of speech, you may want to add extra data as the callee that the caller doesn't care about. Here is an example that I came across in the compiler recently. I often want to have a helper function that processes (say) a struct/enum, something like fn process_enum(def_id: DefId). But then, within the function, I want to iterate over all the variants in the enum, and so I have some variable variant_def_id. Ideally, the def_id of the enum would be enum_def_id, so that the name is clear. But do I want my caller to have to write foo.process_enum(enum_def_id: X)? In that context, it's weird.

And of course separating the names lets you refactor the name internally without breaking clients.

2 Likes

Maybe internal vs. external names could be handled by our existing as-patterns: fn increment(pub by@value: i32). But I think that would only work for Copy types at the moment, if taken literally.

For what it’s worth, its easy enough to add let value = by to separate the names without needing syntax specifically for that purpose.

  1. The feature makes for more-verbose code, which in some cases helps clarify, but in some cases is equivalent to "noise", "boilerplate" or other complaints people have about excess verbosity in languages. Hopefully it is simple to see that the second declaration here is noisier:

     send_onions(amount:int, size:int)
     send_onions(pub amount:int, pub size:int)
    

    And that the second call here is noisier:

     send_onions(amount, size)
     send_onions(amount: amount, size: size)
    
  2. The feature (as-proposed) allows reordering of labeled arguments and optional use of labels on labeled arguments. This particular combination is hazardous because it means that given the declaration:

     fn launch_missiles(pub lon: float, pub lat: float)
    

    the two calls:

     launch_missiles(lat, lon)
     launch_missiles(lat: lat, lon: lon)
    

    mean different things, even though a reader might imagine they are the same call "with clarification", and an author might copy-paste-modify to switch between the two on a call-site-by-call-site basis, based on their own taste. This is a comprehension hazard that I've seen in the wild, in named-argument code in both Ocaml and Python.

I'm not wildly opposed to the feature or anything. I realize it can be done in tasteful ways (I happen to like the restrictions Swift puts on it). I do think that in even its most-restricted forms, it has a cognitive-load tax on basically every declaration and call site, which is .. a lot of places. It's not a thing users can overlook or learn later. It'll be in everyone's face, all the time. I question whether that's a good way to spend the user's mental energy.

Considering that despite its age as a potential feature, it is non-ubiquitous in languages (unlabeled-argument languages keep being produced, used, enjoyed), I think it is not a clear-cut win.

9 Likes

Yes, that's why most programmers in this case write:

send_onions(amount, size)

Or something like:

send_onions(amount: n1, size: s3)

Most function call points will not name the arguments.

I agree, but from what I've seen, new Python programmers usually don't struggle with learning and using this feature. It's easy to understand, remember and use (unless Rust implements this feature in a more complex way, see Swift).

I don't remember being bitten by this problem in years of Python programming... but I agree it could be a problem. One of the main points of named arguments is to make code less bug-prone. If they make the code more bug-prone then the design of this feature has some problems. What about disallowing reordering of the arguments?

It is, indeed.

But this argument makes little sense with the current proposal of making named arguments optional at call-site. If the user deems his variable names sufficiently clear, he can simply use the positional form.

I am a little perplexed by this example, I would rather say that this is a good example for the inclusion of named arguments. The problem here is that the user made a very common mistake of switching the place of two seemingly similar arguments. In this case the named arguments saved the day because it does what the user initially intended!

4 Likes

I think you have missed graydon's point...

It is not about including or excluding named arguments as a general feature. It is an example concerning the hazard of allowing reordering. A user can start with the second (which works correctly) and then, at their option, delete the labels (say because they think the variable names are clear enough), which will cause the code to silently change meaning from correct to incorrect.

2 Likes

I think you’re overthinking this. In most cases name change in the public API can be worked around by a simple ‘let’ statement.

A user might also change the type of arguments.

Realistically, lots of widely used languages have named parameters: Ruby, Python, C#, R and so on. And their users don’t seem to have any problems with confusing the named and positional args.

1 Like

Ah I get your point now, thanks for clarifying :slight_smile:

I have no strong feelings about whether it’s best to allow reordering or not. It would probably feel like a restriction in the beginning, even more so for people coming from languages like Python, Ruby, etc. But I don’t think it would matter that much (for me) in the end.

This is speculation on your part. I indicated experience to the contrary and explained the cause, as well as expressing concern about the general cost of the family of features. You're free to indicate that you don't care about those cases or costs, but it's not productive or helpful argument to just declare they don't exist.

1 Like

I do work with lots of new out-of-college developers and I’ve yet to see them having problems with named arguments. I also work with game designers who are using Lua and not much else, and even they have no problems with named arguments.

I’ve tried searching StackOverflow for problems with named args, but most questions are “how do I force named arguments here” or “how do I do named arguments in N”. It’s quite interesting that pretty much all popular scripting languages now have named args.

It’s also easy to invent doom-and-gloom scenarios against pretty much ANY feature.

The issue doesn’t have to do with novice-ness, it’s a permanent (minor) background cognitive tax / hazard, that applies to all call sites and all levels of expertise. A toe-stub opportunity, like getting operator precedence levels wrong, not a “confused how this works, need to ask SO” issue.

But I’m … kinda not interested in continuing to debate it. I disagree but we’re at the “yes it is” / “no it isn’t” level of discussion, so I’m going to step away from the conversation. Sorry.

2 Likes

The person you're replying to posted earlier in this thread that he personally - as a user of Python and OCaml - has had these problems. He's not 'inventing scenarios,' he's describing his experience:

This, and a lot of the rest of this thread is a good example of why language design is hard and can be frustrating for pretty much everyone involved: for any given feature it is usually pretty easy to provide motivating use cases where the feature improves life in some meaningful way. However, the downsides of it existing are often very, very minor and spread thinly around the language, often as just a tiny delta in the complexity of the language.

This is easier to see in the abstract. Once could imagine proposing ten features which were similar to named fields in terms of some obvious benefit, and some very small extra complexity burden. Now if we added all ten to Rust, we would have a significantly more complex language which would probably be worse off, but each of the ten features alone seems beneficial.

So it can be easy to provide supporting use cases, but very difficult to argue against a 'trivial' increment to the complexity of the language.

I'm not saying we should not add named fields (although that is my opinion in this case), but I am saying that it is not so simple as a cost benefit analysis based on use cases.

Also, this seems to be a very background-dependent argument - if you're familiar with Ruby/Python/etc you probably think this is a useful and simple feature. Coming from a C++/Java background, it probably seems unnecessary and complicated.

10 Likes

This is an interesting point. So we need more Ada programmers around here :slight_smile: