We Have Named Arguments at Home
Steve Klabnik recently wrote about named arguments, optional arguments, default arguments, function overloading, and why most of that design space has historically made him nervous in Rust. I agree with Steve. In fact, I think I agree slightly more strongly than Steve does. :) I actually think we can get most of what we want without adding any new language features. Instead, we can lean into what Rust already provides. None of these is an exact substitute for what you get in Python, Ruby, C++, or Kotlin, but that’s sort of the point. Instead, you can get 80% of the ergonomics without adding any magic to function calls at all. The recurring pattern is that Rust takes something another language puts into function-call semantics and represents it as a normal part of its type system, elegantly sidestepping the mentioned design problems. Let’s revisit Steve’s example from the crate: The obvious problem is that four consecutive s are not a great API that you can reliably use without reading the docs. Let’s assume for a moment that we had named arguments: That’s clearly better, but stable Rust has another syntax in its place: structs. A struct is a named argument with one extra type name. On top of that, we also get arbitrary field order: We also get typo checking, autocomplete, and per-field documentation for free! And we can put invariants on the type and pass the arguments around as values. And, perhaps most importantly, the names belong to the type , rather than becoming part of every function’s calling convention. That last property neatly avoids several problems with actual named arguments. Consider function pointers: What would the parameter names of be? With an argument struct, the question simply wouldn’t come up: If names are semantically important, give the names a type. If they aren’t, then… don’t. There is another delightful benefit here. Steve brings up evaluation order: I.e., should arguments be evaluated in the order they appear at the call site, or in the order the parameters appear in the declaration? Here, does that mean computing before moving or after? Rust already answered this question for structs: Expressions are evaluated where you wrote them. No new rules required. This feels extremely idiomatic to me: rather than teaching function calls a second field-like syntax with subtly different semantics, just use the field syntax that already exists. Of course, declaring a bespoke argument type for every two-argument function would be ridiculous. I would not write: That would be silly. The trick is to notice that named arguments are most useful exactly where an argument bundle becomes conceptually meaningful, which is the same point at which you reach for a struct anyway. These are bad: And these are often better APIs anyway : The design pressure forced us to uncover missing domain concepts. An optional argument is, to some extent, an argument which may or may not exist. Rust has a type for that. This is not as pleasant as: But it has a useful property: the optionality appears in the function’s type. There isn’t a hidden second calling convention for . There is but one function: and every caller supplies both arguments. This is obviously not what you want once you have six optional arguments: I’ve personally been found guilty of this pattern in the past. The problem is that the arguments have stopped being a parameter list and started being configuration. So: Instead of optional arguments, we deal with data. And that adds a nice property: there is no special distinction between “arguments supplied syntactically to this invocation” and “options I calculated elsewhere.” It composes nicely because it’s just a value. Now the obvious objection: writing all those s is terrible. Correct. So don’t. That’s why we have and struct update syntax: That is getting awfully close to: with one minor wrinkle: That is not nothing. But look at what we didn’t have to add: rules for which arguments may be omitted, how positional and named arguments interact, or whether you can omit something in the middle. There’s no special syntax for declaring parameter defaults, no question about whether default expressions run at declaration time or invocation time, and no special representation in types. is just a trait, and function calls remain untouched. Defaults are now usable independently of the function: That is frequently useful in its own right. For library APIs, I often like being slightly more explicit: I think this gets most of the important bits right. Sometimes even the options struct is too noisy, often when construction requires validation or conversion. Then, yes, there is the builder: Steve is right that builders should not be the default. They can become their own tiny programming language. But a small builder has a very useful property: each “argument” is an ordinary method call. That means we can do things like: Doing that with language-level keyword arguments generally requires constructing a map, splatting things, or some other mechanism. In Rust, it’s method calls. I personally find this very pleasing to read. In Java, you can write: Rust doesn’t let you define both: I am very happy about this. But there are several different things people mean when they say they want overloading, and Rust already covers most of them separately. Give them different names: The standard library does this often. See and , for example. This costs the library author one additional name (often just ) and saves every user from doing overload resolution in their head. Use a trait. The standard library does this all the time with traits like , , and . For example: This gives us another useful part of overload-like behavior: one API can accept different input types. For owned conversion: That’s just a single function with one parameter list and trait dispatch. And unlike unrestricted overloading, the relationship between accepted types is explicit: they work as long as they satisfy the bound. That’s a trait, too: That’s polymorphism; we just put it in the trait system instead of in name resolution. Steve’s Ruby example has this equally lovely and terrifying quality: These calls look like they’re invoking one conceptual operation, but they mean wildly different things. In Rust, we can model that directly: That’s more verbose, but in a good way. And I can ask, “Hey editor, what can I redirect to?” and the editor replies with the variants of . That’s more helpful than “read the docs and discover which keys and values this hash accepts.” If we really cared about smoothing down the edges, we could add conversions: And for the structurally interesting case: Remember that this has no runtime cost and is fully type-safe. Not bad for a compiled language. An “options hash” is basically a dynamically typed anonymous struct. So the extremely boring Rust translation is: use a statically typed, named struct. Yes, the Rust version is noisier, but it also detects when we misspell . It’s impossible to pass a string where the status code goes, and it’s straightforward to list every supported option. I don’t think Rust should optimize for making the syntax as dense as possible. Instead, if a set of options is common enough to deserve convenient syntax, it is probably common enough to justify a type. Now, options have a name and the fields can be documented in one place. Rust does not have general-purpose variadic Rust functions. But once again, it already has several ways of expressing the same concept. If all arguments have the same type, take a slice: Or accept an iterator: That is arguably more composable than: because the caller can naturally pass an existing collection. (All type-safe, of course, and with zero indirection at runtime.) If the arguments are heterogeneous, the last resort is to write a custom macro. To be clear, I would not use macros just to fake variadic functions, but I do like how macros can be used in stable Rust, and how the exclamation mark stands out from normal function calls. There is one tiny affordance in all of these examples that I think deserves more credit: field-init shorthand. Rust lets you turn this: This directly addresses one of Steve’s complaints about keyword arguments: Rust’s answer is effectively: In my opinion, that’s even better than keyword arguments. That’s because the labels are still present, the duplication disappears, and nothing needs to change in how we call functions. The thing I like the most about Rust is how each concept nicely interacts with the others. That is not an easy task, and Rust deserves a lot of credit for that. For example, suppose we want a complex HTTP request API with: We could imagine a pile of language features that lets us write: Now wouldn’t that be nice? However, we can already do this in stable Rust with a combination of already existing, composable features: And all we had to do was write the code we’d likely write anyway: Or maybe you prefer a builder? We combined standard Rust concepts: structs, enums, , , struct update syntax, field-init shorthand, traits, generics, iterators, and methods. Those mechanisms are all useful far beyond argument passing. Basic Rust syntax is all the machinery required to build ergonomic APIs. Keeping things simple doesn’t mean worse ergonomics. The obvious response to everything above is: Come on. These aren’t actually named/default/overloaded/variadic arguments. They’re workarounds. All of the above might be useful, though localized, syntax improvements. But the hidden tax is that the language becomes more complex, for arguably little gain. Friction in APIs often pushes us toward solutions that turn out to be useful beyond the original problem: In a sense, the concrete issue points to a broader design problem, and resolving it opens up completely new ways to solve similar problems. That’s great systems design. I think there’s a broader design principle behind all of this. A common design philosophy in dynamic languages is to make familiar constructs more powerful by overloading them with additional semantics. After all, that is one affordance which dynamic typing allows: the ability to decide the meaning of an object at runtime. Rust, however, tends to move complexity outward and let the type system do all the work . One might ask: “In the age of agentic development, doesn’t verbosity become cheaper while redundant labels may make a call easier to understand locally?” I agree with the premise. I’m less sure it changes the conclusion. An agent looking at: gets essentially the same local information. Arguably it gets more: gives the bundle a semantic identity which the function parameter list alone does not. says something useful to both humans and agents. is not merely an optional syntactic argument to this particular invocation; it is a way to configure a request. And if agents really do make typing cost increasingly irrelevant, then the principal downside of these slightly-more-verbose Rust idioms gets cheaper too. The robots can type for me. I’m not opposed to Rust ever gaining named arguments. There may be a proposal that finds a tiny, coherent design which handles patterns, function pointers, traits, evaluation order, compatibility, and all the other sharp edges described. But I don’t feel much urgency. Stable Rust already gives me structs for named options, and for optional values and defaults, traits and enums for varied inputs, and slices and iterators for repeated arguments. Collectively, they cover a lot of ground. And they do it by reusing features Rust already needs. And I think that’s a core part of Rust’s design philosophy: finding the smallest, composable, orthogonal set of abstractions, which, when combined, can solve many problems in elegant ways. The whole is greater than the sum of its parts. Where to go from here Want a second opinion on your team’s Rust APIs? Let’s review the types and abstractions together. a required URL, several accepted URL-like input types, named options, optional timeout, configurable redirects, and a variable number of headers. Actual named arguments might let me turn into: That surely is nicer at the call site. Actual default arguments might let me write instead of introducing . Actual overloading might let two functions share the same name instead of forcing me to invent . These four coordinates become a . Seven random config parameters become . A bunch of dynamically accepted values turn into an enum. A family of related operations becomes a trait. For your team . Training, code review, and architecture support to ship Rust with confidence. For yourself . 1-on-1 mentorship for Rust design, architecture, and code review on real projects.