[Pre-RFC] named arguments

@nikomatsakis could you expand on why you like Swift’s internal/external naming scheme please? I don’t see the motivation myself.

1 Like

So, having slept on this, I think that adding named args to function calls is probably the wrong thing to do. It is not the Rust way to add this kind of feature, we should instead be looking for orthogonal features that can provide similar benefits, and if necessary adding small amounts of syntactic sugar to make them more ergonomic.

We should start by looking at programming patterns that approximate what we want. Here that means passing structs. I think adding default fields to structs is a good start - that is useful in its own rights and gives us a nice way to approximate default function arguments.

Then, should we add sugar to make function calls more ergonomic? Anonymous structs are an obvious path here - they were found to not pull their weight in the past, but perhaps that should be re-visited in this context. Perhaps also (as suggested above) we could add sugar in function defs to elide the _: in some cases.

I’d be interested to hear other suggestions in similar directions for ways we can address the use cases here without adding a very specific feature to the language.

4 Likes

@nrc did you see the last alternative proposal? What are your thoughts on that?

I did, I think the sugared form still has the same problems with parsing that the other proposals have. Using a trait is intriguing - it is a nice way to opt-in to using a struct. But I think the desugaring here would be complex, it’s not clear to me how using the trait simplifies the desugaring vs the original proposal.

Ah, yep, you're right. I figured I was missing something, and so I was.


I'm not Niko, but I can say for myself that I've often wanted similar in Python. What you want to express to a caller vs. what you want to call something within the body of a function aren't always the same. The increment example given above is fairly canonical in that regard. You end up with a call that reads very clearly – increment(4, by: 3) – and then internal to the function you're not left writing something like amount + by. That lets you be more succinct while also being clear. The alternative is to use a name like byAmount, which definitely works but is obviously much longer.

If you put those two things together, you get very close to what has emerged as a fairly standard pattern in JavaScript over the last decade: the options argument. That in turn makes e.g. the example cited by @BertrandR of math modeling functions much more doable. In modern JS, where you can supply default parameters to functions, you might even write it like:

function increment({ amount = 0, byAmount = 1 }) {
  return amount + byAmount;
}

increment({ amount: 1 });  // 1 + 1 => 2
increment({ byAmount: 5 });  // 0 + 5 => 5
increment({ amount: 3, byAmount: 5 });  // 3 + 5 => 8

I actually find myself missing that pattern when writing Python, as I do think it's superior to named arguments.


Aside: if we did it that way, it might also be a good time to look at stealing modern JS' ability to do object creation shorthand in struct creation; that lets you write this:

let amount = 4;
let byAmount = 6;

// shorthand
increment({ amount, byAmount });

// instead of longer form
increment({ amount: amount, byAmount: byAmount })
2 Likes

My problem with the ObjC/Swift “named param” style is that at least over there it isn’t just used, it’s abused to the point where I can’t even recall the bloated 40-char fn name I used not 5 minutes earlier (Apple also has this bloat problem with their documentation so it might be indicative of their culture as a corporation). That is a road I would prefer to see Rust not take, being succinct in what you express is generally a good thing in my experience.

1 Like

If you do care about making things more explicit, you can already very easily implement your API such that instead of this:

http.get("Rust Programming Language", 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);

your users must write this:

http.get(url("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));

by just using newtype which is just a one liner. This is not considered in the alternatives section, and should definitely be in it.

Since this is already both easy and possible, I think that adding extra language support for this is not necessary. So I am against this pre-RFC as is.

Having said this, the only situation in which named arguments seem to be useful is when dealing with optional arguments. This is used extensively in Python, where a function taking multiple positional arguments might have a lot of defaulted arguments but the user might just want to override one of the defaults.

That is, the only way I can think of properly motivating this RFC is with default arguments in mind.

Now, even if the RFC was properly motivated to me, I think that, since argument names are part of the contract between a function and its callers (that is, changing them breaks code), the argument names must be part of a function's type. This sounds like a bad idea, and leaves a lot of points to be resolved: how do you specify them in type signatures? Can you constraint on argument names using traits? How far should type erasing go? Would it be possible to cast function types with names to function types without names (e.g. to interface with C)? Are these casts safe/unsafe explicit/implicit ? How does this interact with trait methods, default methods (specialization)... Do all impls of a trait need to use the same argument names of the trait?, do specializations need to do the same?, can traits abstract over argument names? (FnOnce(f32, f32, SomeName: u32) where SomeName is itself generic, that is, it doesn't matter which name it has, just that it has a name).

This feature goes from small to large when one starts solving all these issues. Even if this would come with default arguments and play well with it, I don't know if it would be worth it, but I encourage you to work on an RFC that solves this and explores the design space with default arguments combined to check it out. I like named arguments + default arguments in Python, so I can imagine how this would make Rust a better language.

While this is possible, it is not as ergonomic.

  • The user has to import all the newtypes.. Inflating the use lines or requiring a glob import (à la prelude)
  • You loose the positional form, now the user has to wrap all the arguments
  • Nothing forces the user to wrap the types in the function call, rendering this useless or even an extra layer of indirection.
  • Newtypes don't make the intent clear, as a user I wouldn't understand that the use of newtypes is because the author want's me to use explicit arguments...
  • The order of the parameters is still important
  • ...

I'm ok for adding this as an alternative, but I don't find it a convincing one :wink:

It's part of the motivation. The problem with default arguments is that it causes a lot of debate. There are technical constraints that have to be figured out and discussed. Builder pattern and struct advocates are even more strongly against it.

So I figured it would help move things forward to add this in a more incremental way. I know named and default parameters are often considered together and often one has limited use without the other. But they can still be used separately. The end goal is of course named + default parameters.

If people want to consider both at the same time, that's totally fine with me too. :slight_smile:

Are function names also part of the type? I am not convinced adding it to the type would be a good idea. It would add a lot of constraints for little benefit. But I'm not knowledgeable enough about the type system and the compiler internals to say anything for sure.

4 Likes

It is not the Rust way to add this kind of feature, we should instead be looking for orthogonal features that can provide similar benefits, and if necessary adding small amounts of syntactic sugar to make them more ergonomic.

Is "the Rust way" articulated anywhere, for reference?

Fwiw, I'm generally against this feature. I know it helps with explicitness, but I think the cost in complexity to the reader (noise + number of different ways one can say the same thing) outweighs it. We went over it several times in the past; while it's an aesthetic / subjective call, it never seemed quite worth the cost.

Is "the Rust way" articulated anywhere, for reference?

Probably not yet, but here, I'll make one up:

  • Safe is better than convenient
  • Kind is better than truculent
  • Fast is better than flexible
  • None is better than chaotic

Or perhaps just: "when in doubt, leave it out."

I may, as a final inspiration, suggest the manual reintroduce the quote that used to adorn its introduction: Rust Documentation

We have to fight chaos, and the most effective way of doing that is to prevent its emergence.

  • Edsger Dijkstra
14 Likes

I'm generally very much +1 in favour of named arguments. There's probably a lot we can learn from Swift too.

Once you make an argument public, the user can use both forms (positional or named). This allows API authors to "promote" positional arguments to named arguments without any breaking change.

What is to stop the author removing the pub annotation, or renaming the parameter? This would presumably be a cargo/lint check comparing with the previously released version? I guess I just don't see pub as a strong benefit.

The pub story seems to be really the tip of the ABI iceberg. We don't (AFAIK!) have a mechanism for enforcing or checking interface compatibility now. So this seems to be part of a larger discussion. How is it different to what might happen if you changed the name of a field in a struct, or a function name?

At the moment, I'm inclined to think that a breaking change is a breaking change, whether a parameter name or something else.

Can we leave out macros then?

Just my two cents, semi stream of consciousness :stuck_out_tongue:

Love named/labelled arguments, I think it’s crazy they’re not in the language. I program OCaml professionally. It just comes up all the time, fairly often, you’ll have two ints; or two strings; it’s just a disaster waiting to happen when they aren’t named, sorry. It essentially self documents the code, without documentation… In huge codebases, older code that’s not maintained in a while, this kind of stuff comes up, and labelled arguments are just great when they’re there.

In fact there are named and unamed variants of some modules in the stdlib, and other standard lib extensions usually add labelled versions, just because they’re so universally adored :smiley:

Rust wise, syntactically, I really like the pub feature in the declaration, I thought it was very elegant reuse of the word, immediately conveyed to me it was a named argument. Nice idea! :thumbsup:

I have breezed this post only a couple of times, apologies if this was mentioned, or if it’s an RFC (or even a syntactic feature, though I don’t immediately see it :P) but taking another page from OCaml’s labelled arguments (on the caller side, not declaration side), the ~ symbol is used to express labels/named argument parameters:

  1. calling a function with labelled arguments is: foo ~my_int:10 ~another_one:(10*3) 10.0, which will be a function foo called with the two labelled arguments specified via ~<label>:<value> syntax
  2. an extremely useful piece of sugar I take for granted daily is that when an argument passed to the label is the same as the label, the value assignment can be omitted. I.e.,
let my_int = 10 in
let another_one = 10*3 in
foo ~my_int ~another_one 10.0

This seems dumb but my oh my it’s magical; I find myself creating variable bindings just so the value assignment can be omitted; the code comes out cleaner, everything looks nicer, etc. It’s another version of “type punning” that OCaml uses a lot (record field name type punning is another piece of sugar I honestly sorely miss, though as I was instructed a while ago, “just write a macro” ;))

so in Rust we might do:

pub fn foo (pub my_int: u32, pub another_one: u32, _stupid_float: f32) -> u32 {
   my_int * another_one
} 
//
let my_int = 10;
let another_one = 10 * 3;
let res = foo(~my_int, ~another_one, 10.0);

This looks funny as I write it but I’ve typed too much to go back :wink:

Or just brainstorming, we could get crazy and do:

let res = foo(my_int~10, another_one~(10*3), 10.0);

(sidenote, I personally dislike => suggestion; it looks strange and heavyweight to me, one too many glyphs.)

Anyway, just my thoughts, labelled arguments FTW, that and everything else feel free to punch apart :stuck_out_tongue:

1 Like

Tempting perhaps. But they are there an escape hatch, to allow leaving out other things.

Anonymous structs might really be the best option here at the moment.

The implications for closures with named arguments aren't that nice. Either named arguments for closures are forbidden - or the caller of the closure is forbidden to use them - making them second class citizens, or the named arguments are somehow part of the closure type, because otherwise the receiver wouldn't be able to call it with named arguments. This would certainly increase the friction how interchangeable functions are.

How does OCaml handle this case?

2 Likes

labelled arguments (and optional arguments to boot) are apart of the functions type (whether a closure or not), e.g.:

let foo ~x y = x * y

has the signature:

x:int -> int -> int

If we have that named arguments can be supplied either by-name or positionally, then I don’t think there’s anything wrong with saying that when you cast a function with named parameters to an fn or Fn type, you retain only the positional capability.

In other words:

  • If you give a function named parameters, you gain the ability to supply those arguments by-name instead of positionally.
  • This is strictly additive. You can still supply the arguments positionally just as before, and use the function in any other way you could have before.
  • This includes casting the function to an fn or Fn type, which are unchanged, and still only support positional parameters.

And then we can avoid opening a can of worms. If we decide that named parameters in fn and Fn types are something we want, we can always work on adding them separately, later.

3 Likes

Myself, I’ve never wanted named arguments when writing Rust, even though I use them all the time in other languages. I don’t feel like Rust lends itself to writing the sort of functions in which named arguments appear in other languages. I also think that types probably aleviate some of the pressure - many times named arguments take untyped values (such as strings) as arguments, where in Rust they would often take a more self-documenting enum.

I am still unsure if I want named arguments in Rust. But a person can't dismiss this idea quickly.

D language so far has refused to add add this feature not because of complexity (Python programmers learn to use this feature quickly, and they understand quickly why it's sometimes useful), nor for the noise: currently there are cases where I think Rust is too much explicit and repetitive/noisy:

enum MyFooEnum { Foo, Bar, Spam, Maz }

fn main() {
    let f = MyFooEnum::Bar;
    let x = match f {
        MyFooEnum::Foo => 3,
        MyFooEnum::Bar => 6,
        MyFooEnum::Spam => 9,
        MyFooEnum::Maz => 2,
    };
}

If this repetitive and noisy code is idiomatic for a language, then it's harder to refuse named arguments for being too much noisy, because giving argument names is probably less useless than repeating the enum name every time, and in the cases where adding names is useless and just add noise, most programmers just don't add those names, giving a name to an argument at the call site is optional.

And while I agree with the Python Zen rule "There should be one-- and preferably only one --obvious way to do it.", in this case I think it doesn't apply much, because named arguments don't change the meaning and semantics of the code much, it's mostly just a way to assert more explicitly what you want to give to a function.

So far D language has refused to add named arguments because they fix the function arguments in the API. Scala language faces this problem with the @deprecatedName I've shown above:

def inc(x: Int, @deprecatedName('y) n: Int): Int = x + n

Regarding the explicitness, looking at Ada language, Ada programmers say that giving names to the function arguments at the call site makes the code less bug-prone. Surely in Python giving names to arguments has made my code more self-explicative and less buggy (but Python is a language rather different from Rust, so you can't compare them directly). I think Rust could be eventually be used in the same "high integrity" coding situations Ada has being used so far, so the argument of Ada programmers could be significant.

Some people think that adding named arguments to a language encourages programmers to create functions with many arguments. I don't know if this is a true effect.

Overall I think I'd like named arguments in Rust, I am going to use them sparsely only whey they add value, when the code is less clear, or when I want extra explicitness. But I can live without this feature and for me they aren't near the top of the features I'd like for Rust.

1 Like

I apologize if I gave the impression of dismissing it quickly. I've been over it many times with many people and seen many implementations. I just have come to the conclusion that its costs feel a little beyond its benefits. Similar to how I feel about (say) array types with adjustable dimension-limits, or language-provided subrange types (both popular in Ada as well). I absolutely understand the utility of this feature, and I even think the current proposal (overloading pub) is relatively elegant compared to previous ones.

On balance I'm still (slightly) against the additional costs, but I think it's important for the community to make up its own mind using its own governance structure (which I am not even a part of!) So please don't hear me as trying to overrule that process. Perhaps I can contribute constructively as well:

Lately I've been working in Swift a fair bit and it does this, indeed, as Niko said. In fact it supports ad-hoc overloading, and overloading by different argument labels rather than just argument types, along with supporting default args and variadics. So it goes quite a ways further than I'm comfortable with, in terms of leveraging this stuff. But it also has a fairly tight set of restrictions on the way the labels are used, which (if you're going to pursue this feature here) I'd recommend copying at least some of. Specifically:

  • Arguments have an internal and external label (equal by default, with _ meaning "omitted")
  • Labels are part of types, with auto-coercion for 1st class values with different labels
  • Call sites must provide labels that exactly match external labels of callee type
  • Including the presence (by an ident) or absence (by an underscore) of a label
  • No argument reordering or optional providing/omitting labels at call sites

Obviously from a backward compatibility perspective you can't opt all Rust code into requiring labels on all call sites to existing functions, so you'd have to change the default reading of a callee; but the restrictions on labeling at call sites seems to simplify the feature, to me.

In particular, the way in which many implementations of this feature have to pick a "cutoff point" in an argument list between the permutable and non-permutable ("positional") arguments, based on inspecting the labels-provided at call site and labels-allowed at callee, seems like a major comprehensibility hazard. Swift's rules are adequate to eliminate argument-ambiguity hazards when they exist, without introducing new ones due to flexible ordering/labeling.

2 Likes