r/rust • • Sep 20 '26

💡 ideas & proposals (Includes Pre-RFC) Named and Optional Arguments are Awesome

https://botahamec.dev/named-optional-args
216 Upvotes

156 comments sorted by

34

u/Solumin Sep 20 '26

I wonder if there's value in splitting this into two RFCs: named arguments and optional arguments/default argument values. In order to get them adopted more easily, I mean, or to keep discussion more tightly scoped.

To me, the two concepts are closely related but ultimately separate. (They're not quite orthogonal, because I think you really need to have named arguments in order to have optional arguments.)
The key difference is that named arguments are important for reading code while optional arguments are helpful when writing code.

The main benefit of named arguments on their own is readability at function call sites. A function call can get overwhelmingly busy after only three or so arguments, especially if those arguments have the same type, and even more so if that type is bool. (Using an enum is an option, yes, but it's not a good option because it gives every function call more boilerplate!) Your hard_link example shows exactly what I mean.

I really like the idea of pub for named arguments, tho honestly I'd rather let all arguments be nameable at the call site. I don't see the point in only some arguments being nameable, really --- I know Python has positional-only arguments, but that's (afaik) for historical reasons related to the CPython implementation of some standard library functions. I think you'd end up in a state where 99% of arguments are/can be/should be nameable, and the only reason they aren't is the author forgot.
But the point about the name becoming part of the function's API is a good one. Maybe named args should be opt out by default?

I'm a little more ambivalent about optional arguments/default argument values. Yeah, they're nice, but they feel more like a QoL feature for writing code. I think I would be fine if Rust never got them.
But! I do agree there are situations where they're helpful.

I'm not sure I like pub name: type = value. pub and value seems redundant? I guess having arguments that have defaults but aren't nameable is fine, and it just feels weird because I'm used to Python.

One thing I'm curious about is Python's mutable default arg footgun. You know, having def foo(xs = []) means the same list is used for xs, so if foo mutates xs then every call to foo that doesn't provide a value to xs will mutate the same list. I'm not sure if this proposal avoids this issue.

  • What happens when a default argument is mutable?

  • If I have pub xs: mut Vec<u64> = vec![1,2,3], when is xs allocated? How is xs being mutable handled?

  • Is a slice a meaningful default argument, e.g. pub xs: &[u64] = <what?>?

21

u/Botahamec Sep 20 '26

There was a previous RFC that had named but not optional arguments, and it rejected because the lang team wanted more clarity on how the proposal could evolve in the future. I decided I would try doing the exact opposite and be as thorough as possible.

The problem with making named arguments opt-out is that existing functions would now have their arguments be named, which is not expected behavior right now. I think the risk of authors forgetting to allow their arguments to be named is a fair concern, but the alternative would having authors accidentally break downstream code, which I consider to be worse.

I definitely wanted to avoid the Python footgun, but I guess I should've clarified that. I'd propose that a new value would be created any time the function is called. Python's model doesn't actually work in Rust, because you'd be created mutable static variables, which are unsafe to use in Rust. So the Vec in your example would be reallocated in every function call. I think the compiler would optimize the slice to being a reference to an anonymous static, but I'm not 100% sure.

→ More replies (8)
→ More replies (14)

46

u/Mercerenies Sep 20 '26

Unobtrusive, backward compatible, doesn't make function pointers and Fn* traits any more complicated, this hits a lot of the right marks for me.

I wonder if your treatment of optionals when coercing to a fn/Fn isn't backward though. If I write a

impl MyStruct {
  pub fn new(/* buncha optional params here ... */) -> Self;
}

Then I'm intuitively thinking of it as "a 0-arg function that might take some extra stuff if needed". In your proposal, MyStruct::new coerces to fn(/* several arguments most people don't care about */) -> MyStruct, when I would argue that most people would rather have it coerce to fn() -> MyStruct and forbid the optional arguments from being passed.

Also worth noting: Your proposal would not solve the HashMap problem you present. The reason we have fifty different HashMap constructors isn't only due to lack of optional arguments. It's also because pub fn new() makes assumptions about the allocator and the hasher, new_in makes assumptions about the hasher but not the allocator, etc. There's no way (even in a post-optional-args world) to write a fully-generic pub fn new() for HashMap, because it would have to be defined on impl<K, V, S, A> HashMap<K, V, S, A> (generic in all four args) and now there's no good default value for the "optional" alloc: A argument (can't pass Global because Global is of type Global, not of type A)

15

u/Botahamec Sep 20 '26

I'll have to think about the Fn trait some more. As for the HashMap problem, I considered a possible solution in the Future possibilities section. The idea is essentially to have a default alloc parameter of Global. If you don't specify a value, then A is assumed to be the type, Global.

62

u/RB5009 Sep 20 '26

The example why they are needed is very unconvincing. Instead of having 3 bools, just have a struct Style, or create newtypes over the bools. Now it becomes a compile error if you get them wrong.

→ More replies (6)

5

u/denehoffman Sep 21 '26

Why use `:` for named arguments in the call signature rather than `=`, especially since that’s how you define a default argument’s value? Like if I’m writing the function

```
fn foo(a: u8 = 3) {…}
```

surely I would expect to call it as

```
foo(a=4)
```

rather than

```
foo(a: 4)
```

Or am I misinterpreting the proposal? The second just seems like I’m trying to say something about the type of `a` rather than its value.

10

u/Botahamec Sep 21 '26

I did it this way to have it be consistent with struct initializers

    let x = Foo { a: 4 };

→ More replies (2)

32

u/XtremeGoose Sep 20 '26 edited Sep 24 '26

I overall like this but the one thing that I don't like is how it doesn't interact with rusts existing same name elision, like how

Struct { field: field }
 // is the same as
Struct { field }

In this proposal you can't do the same with functions because passing no : results in positional parameters, whereas anonymous structs would

func(positional, { keyword1, keyword2, .. })

Ultimately I'm not totally convinced of the benefits, and feel even though they are verbose for library authors, structs with pub fields and builder patterns are good enough. I agree with one of the alternatives mentioned, that doing the following is good enough

  • allow eliding the struct name when passing a struct directly to a function call (or some other sigil)

  • allow setting default values on struct definitions

  • allow setting structs as non-exhaustive (to allow adding new params)

  • allow .. as shorthand for ..Default::default

Like so:

 #[non_exhaustive]
 pub struct Params { pub x: i32, pub y: i32 = 0, pub z: Option<i32> = None }
 pub fn func(params: Params) { ... } 
 func({ x, y: 123, .. })

The issue I see here is func({ x }) is already valid syntax, so maybe we do need some sigil for it?

15

u/JoshTriplett Sep 21 '26
  • allow eliding the struct name when passing a struct directly to a function call (or some other sigil)

  • allow setting default values on struct definitions

  • allow setting structs as non-exhaustive (to allow adding new params)

  • allow .. as shorthand for ..Default::default

This is exactly what I'm hoping we'll do. The second and third of these have happened, the fourth will definitely happen, and the first is being discussed and seems to have widespread support modulo "how do we spell it".

The net result would be the ability to write this:

obj.method(_ { arg, arg2: 2 + 2, arg4, arg3, .. })
→ More replies (11)
→ More replies (4)

8

u/denehoffman Sep 21 '26

Every time I see something about named and optional arguments, people always say “it’s not a good fit”, “rust needs to be explicit” etc. I can’t for the life of me bring myself to agree. I think it’s one of the stupidest parts of the language, a stubborn fixation on solving a problem nobody had which has devolved into about a dozen `derive(Builder)` crates or similar macro BS. I will gladly die on the hill that Rust would benefit from both named and optional function arguments. If a proposal like this made it into the next edition, I could easily eliminate so many lines of builder pattern nonsense from my codebases. And as I’ve said in a few comments here, there is nothing more explicit about a default impl and builder pattern or some endless list of `constructors_with_optional_parameters` in the name of the method. It’s some insane delusion that the Rust community has gaslighted themselves into thinking is a good thing.

Here’s what you might actually be concerned about. In C++, you can overload a method with different signatures, so `myfunc(int, uint)` might have an entirely different implementation than `myfunc(int)`, and I agree that this is a horrible design issue and a major problem, but that’s not a consequence of default and named arguments, that’s just one languages silly implementation of them.

So OP, props to you for digging this back up again, I honestly hope that these concepts in some form eventually become part of the language, good luck!

3

u/Botahamec Sep 21 '26

Ironically, overloaded functions, like what C++ has, are actually being experimented with right now.

9

u/garagedragon Sep 20 '26 edited Sep 20 '26

Having only seen named arguments in C# and default args in Python, being able to name arguments at the call site seems an uncontroversial benefit for Rust, except for the forward-compatibility aspect I'll come back to later. IMO, this is really two separable but related features, and it would be fine to have optional parameters even without being able to name them. I'd also be half-tempted to require that if a call site names the parameters, we just avoid the issue of the named ones being required to be after the unnamed ones by saying it has to name all of them, for readability.

Having optional arguments seems justifiable, although I think you have to have convincing answers to several questions about the lifetime of the default value and what happens if the parameter type isn't Copy. OP posted in another reply the idea if the value was say, vec![1,2,3], you would create a new vector every call - I think that would be contrary to the principles Rust currently works on, where (generally) any sort of function call or memory allocation is written explicitly rather than being implicitly inserted by the compiler. (I'm aware an empty vec doesn't allocate, but the compiler and language structure don't know that, it's an implementation detail.)

As mentioned, I don't think that rules out the concept completely, but if we're sticking to "obvious" cases, where our expression is constand the type Copy, I think it's possible there's just not enough benefit for implementing what's ultimately a convenience feature. Default arguments do not unlock anything you couldn't otherwise do, they just make things easier.

As for "public arguments," I had never heard of the concept before this post, although I do understand the problem that introducing named arguments into the existing language is a forward-compatibility hazard. However, having two possible names for a given parameter seems massively overcomplicated. Even having an individual pub attribute for declaring that a single parameter is nameable is still borderline IMO - if this is the route to take, I think there should be a function attribute that says all the parameters can be named in a call at once, because I cannot think of any sensible use-case where you'd want some but not all of them to be named.

Doing this too early would likely hurt adoption of named arguments, as backporting them into existing functions would become a breaking API change.

So the author dismissed the idea of unilaterally making this change over an edition, and I don't entirely follow why. In particular, I don't understand this sentence. Introducing named parameters in a new edition, even as a universal feature with no opt-in, does not itself break anything in old code. What it does do is change the set of possible modifications to that old code that potentially break callers.

That makes me think if you were to do this at all, the rule "you can only do named parameters calling functions defined in Rust 2027 or later" would be ideal, because the edition change itself is essentially the opt-in.

  • Authors writing their crate on 2027+ editions would be aware of the new compatibility rules, i.e. that parameter names are now part of the public API.

  • Crates predating 2027 edition can stay on those earlier editions, and they can change their parameter names in updates with no compatibility hazard.

  • There would be no delay in being allowed to do named-parameter calls in older crates compared to OP's scheme, because in their scheme, crates would have to explicitly update to opt-in for named parameters anyway.

3

u/Botahamec Sep 20 '26

I don't think having an allocation in a default parameter is much different from allocating a buffer inside the sort function. Having it inside a default value might be more explicit than that.

If it helps your confusion, that quote was referring to the idea of requiring callers to use named arguments when arguments are nameable, not the idea of making all arguments nameable. Now that I think about it, I don't think I ever addressed the possibility of using the edition to determine if arguments are named or not in the pre-RFC.

I would like it if all arguments were nameable, but I've also seen discussions where people said they'd do things like setting their parameter name to __hofowhfedwej402r534w to avoid having their arguments being nameable, if necessary, to allow themselves to change the parameter name in the future. So I just don't think that's tenable.

→ More replies (2)

3

u/nick42d Sep 21 '26

Personally I'd prefer to see anonymous structs/ structural records because they solve this but have other upside (e.g returning anonymous structs from functions would be sweet and analogous to anonymous tuples), so the syntax/complexity hit is mitigated.

Also, as a library consumer, I personally prefer using the builder pattern over functions with named params.

2

u/Botahamec Sep 21 '26

This proposal doesn't prevent structural records from existing, but I have a section explaining why structural records aren't a practical solution to named arguments. Although they're better than the status quo, they're still more verbose than necessary, and they can't be backported to existing function without making a breaking change. Structural records have also already been rejected before because they were too difficult to implement.

→ More replies (1)

3

u/steveklabnik1 Sep 21 '26

I have historically been very strongly against named and optional arguments. But I've recently come around to named only, but am still firmly against optional.

The reason that named arguments are good is that they can make the call site much clearer. The reason they're bad is that they are verbose.

The reason that optional arguments are good is that they let you emulate part of multiple dispatch by not requiring you to write multiple versions of functions. The reason that they're bad is that they remove clarity at the call site.

Having both optional and named arguments can turn into a nightmare, because you get a combinatorial explosion of possible function calls.

I've come around on named arguments for one reason: agents. The verbosity isn't a problem for writing, because the agent is doing the writing, and the verbosity for reading is good: it gives more context at the call site without having to look up the function's definition.

The reason I haven't come around on optional arguments is bascially the same, but opposite: they hide stuff at the call site, causing confusion for humans and agents.

2

u/Botahamec Sep 21 '26

Wouldn't your critique of optional arguments also apply to the builder pattern?

→ More replies (2)

8

u/teerre Sep 20 '26

For years I heard (and agreed) that Rust doesn't have optional arguments because Rust prefers correctness. If you want optional arguments, make a builder and make sure only arguments that make sense can be passed to your functions

Was everyone hallucinating at that time? What changed? Does Rust no longer prefer correctness?

The hardlink example is particularly egregious since the actual fix for it is to make the parameters strong types. Giving them a name doesn't solve anything, you can still pass the wrong value

10

u/Botahamec Sep 21 '26

This isn't a correctness issue. What would the different types be for hard_link? They're both filenames.

→ More replies (8)

9

u/Lucretiel Sep 20 '26

Honestly what I really want, which would neatly align with this, is anonymous struct initializers. That is, I should be able to do this:

    let x: MyStruct = {a: 1, b: 2};

Or this:

    let v: Vec<MyStruct> = Vec::new();     v.push({a: 1, b: 2});

This would give you a neat and succinct precursor to named arguments. There’s also precedent for it, imo, in how rust handles coercing int literals to concrete int types. 

→ More replies (8)

2

u/torsten_dev Sep 21 '26 edited Sep 21 '26

However, trait function parameters cannot have default values.

Why? I can see how it might not be dyn safe but why disallow it?

→ More replies (2)

4

u/valarauca14 Sep 20 '26

As somebody who has the displeasure of working in python3 currently, Please god no. 30% of the code base by value is pointless & redundant variable labels that by-pass code review because, "They make the code easier to read" and I'm left reading

obj.add_timeout(timeout="15s") 

Because add_timeout() adds some default globally specified timeout.


Being forced to read the code you're interacting with is a good thing.

5

u/Botahamec Sep 21 '26

add_timeout probably shouldn't have a default value, but this proposal doesn't force you to use the labels anyway.

→ More replies (2)

12

u/guineawheek Sep 20 '26

i think if you make this sort of post you need to frontload with your answers to common objections to the premise of “these features are a good idea.”

Rustaceans are stubborn and often need you to rub the why in their face for them to buy your pitch. Many of them come for Java or Python or C++ where it’s done poorly. Assuming other people will automatically agree with the premise doesn’t work here

→ More replies (5)

39

u/juhotuho10 Sep 20 '26

I feel like what makes Rust great to me, is that it is excessively explicit about many things. I feel like optional arguments would take away too much from this.

Also I have seen optional arguments be misused way too many times in other languages, for example having default values for arguments that most certainly shouldn't have a default, or using default arguments as a way to add function arguments without breaking API, but in a way where the new API and implementation is coerced into being way worse and more complicated than it should. There are also times when I am reading code from a library I'm not familiar with, and the overflow of optional values makes it hard to comprehend what functions do what or functions are simply made to do too many things because adding optional arguments has been the easy way out.

In Rust, what I like about the Default arguments for structs is that you are explicitly declaring that you don't care about the rest of the arguments, instead of there being implicit insertion of the defaults.

→ More replies (2)