I’d love to see a language with structural types where functions are always unary and tuples don’t exist. Every function call would look like either
f ()(no args),f 1(one arg) orf (foo: 1, bar: 2)(multiple args, names mandatory).I started building a language like this but I haven’t worked on it in a while.
Who is even the target audience of this post? Beginners (which included all of us once) ask for all sorts of things. But these features didn’t happen in Rust for a reason.
Named parameters are not needed in the age of LSP hints, where this exists as an editor feature (including (neo)vim).
But if you really want named parameters AND optionals/defaults, rust always allowed the args struct pattern:
struct Args<'a> { a: &'a str, b: u64, c: Option<u64>, } impl<'a> Default for Args<'a> { fn default() -> Self { Self { a: "hi", // default b: 3, // default c: None, // optional default } } } takes_args(Args{a: "not hi", ..Args::default()})The builder pattern is more about valid initialization, but does interact with the arg struct pattern when you have non-default (forced) arguments, although even then, you can not use it by having:
struct Args<'a> { forced_arg1: &'a str, forced_arg2: u8, defaults: DefaultArgs, // implements Default }So mentioning the builder pattern is out of place.
Overloading is not used because we have traits (and the sound subset of specialization for when that’s needed).
So all in all, those “features” didn’t make it to Rust not because of the backlog, or for simplicity’s sake, but because idiomatic Rust (and even modern tooling in general, a la LSP) have alternatives that give you what you want and more.
Everything mentioned above should be known by anyone who knew Rust for more than a month.
Steve doesn’t really elaborate on the drawbacks to the features he listed, just kinda says he doesn’t like them. I’ll do some of that here:
- Function overloading: overload resolution adds a ton of complexity to the language for a feature with API-friendly workarounds (use different names, builders, etc).
- Default/optional arguments: can be confusing when the default value is initialized. For example, in Python, default values are static and reused across calls, meaning you can technically have
def foo(a=[]), append toaon each call, and use that data on future calls tofoosince the same list is provided as a default each time. In C#, default values are compiled into the caller’s code, meaning if the caller’s code was developed for an older version of a library and you use it with an updated version that has a new default, the caller will still use the old default. And so on, languages all handle defaults differently. - Named parameters: the article covers some of the biggest issues. There’s also the issue that API designers may not want their parameter names to be part of the API (consider the function is generated by a macro or build script, for example). Also, named parameters imply to new Rust devs that the parameters can be reordered, and it’s odd to have them without optional arguments as well. Then there’s the issue of code inspection (via macro or tool) if you can reorder parameters since you can only inspect source tokens, not query semantic information, so if parameters are reordered then it can become difficult to know which value goes to which parameter from the macro/tool.
- Varargs: to be honest, I don’t really have a drawback for this in mind beyond them just being a fair bit complicated to implement. The C#
params Foo[] argsequivalent wouldn’t be very useful, but varargs with variadic types would be powerful, just I assume very difficult to implement (for examplefn foo<...Ts>(blahs: Ts...)or something, whereTsis a variadic tuple). In any case, that’s a feature many libraries would take advantage of (axum, bevy, etc).
Default/optional arguments: can be confusing when the default value is initialized.
Yeah, which especially becomes an issue in languages with poor mutability controls. Probably the most benign variant of argument defaults would be restricting them to being
const, or at the very least immutable including internal mutability. Instead we have situations like in Python where it’s necessary for linters to warn people that setting[]or some other mutable data structure as a default is a bad idea (e.g. B006).There’s also the issue that API designers may not want their parameter names to be part of the API
Yeah, this is also something of a mixed bag, where
- there are some cases where having the parameter name as part of the API is desirable,
- e.g. if some parameter position both before and after a change is a
T, but it’s used for different purposes, - though it is unclear whether that can’t always be better covered by using the type system better;
- e.g. if some parameter position both before and after a change is a
- while in most cases being locked out of doing some trivial renaming for whatever purpose (like
s/blacklist/blocklist/) because it would break clients is just a PITA.
I wrote in another comment that I was partial to named arguments out of habit, but the more I think on it, I wonder if it’s not mostly a tool to work around missing type information and bad APIs, which, uh, may not be the most desirable thing to add to Rust.
- there are some cases where having the parameter name as part of the API is desirable,
I’m used to named arguments from other languages, and that makes me fairly partial to them. Having to tool around with builders feels a lot more complex and clunky.
That said, I do wonder at how many of the usecases aren’t workarounds for long argument lists that are either stringly typed, or something similar. As in, with a signature of
(i32,i32,i32,i32) -> whatever, named arguments seem like a way of getting the compiler to catch errors in confusing{x,y}_{position,length}; but if the signature were newtypes likeRect(Origin(i32,i32), Size(i32, i32)) -> whatever, then the felt need for named arguments drops considerably.With newtypes you can also emulate named args with a single struct. This also allows them to be passed on together.
struct RectArgs{ x:i32, y:32, w:32, h:32, } impl Default for RectArgs{...}Then the call site looks like this:
rect(RectArgs{ x:0, y:0, w:30, h:20, } // with default arguments rect(RectArgs{ w:30, h:20, .. RectArgs::default () }Yeah, though in those cases you might get an extra question about why
rectis a function and not a method, e.g.Rect { w: 30, h: 20, .. RectArgs::default() }.do_the_thing()I assumed the rect is a method on something like a graphics context or physics engine, but yeah.
Yeah, decent assumption, I’ll tone down my comment a bit.
Newtypes have their uses, but as a replacement for named arguments they are an overkill in many cases. They solve different problems.
Newtypes are beneficial when some domain-specific value with an invariant is used extensively throughout the codebase. You would use them to store and pass around that value without fear that something goes wrong.
However if we start to create a type for each parameter of every function (which may not be related at all), it would only result in a lot of unnecessary boilerplate. Named arguments is a simple and elegant solution for calling a function in a more explicit way, nothing more.
No.
YES!
Is this the 5 minute argument?
Well it WAS the 5 second argument.
Is it when you pick up an argument someone dropped?
I don’t know, it looks like the amount of issues is way higher than improvement in comfort. Beside, they mentioned a lot of corner cases that would definitely show up and become a problem
I’ve been using C++ for quite some time, and it also had some talks about having named parameters, but it only helps in some places and hinders in most, imo. I feel like in Rust you may use a struct as parameter containing all of function parameteres where you really want the names
Maybe
This greatly depends on language design and implementation.
For example, Clojure has all of these and much more, and it’s still much simpler and hassle free than rust. The functions are just functions, but the forms and lists that are very powerful.
functions are just
(defn foo [a b] (print "do thing")) (foo 1 2)With the power of destructuring that maps provide us, we can have named arguments:
(defn foo [a & {:keys [b c]}] (print a b c)) (foo 1 {:b 2, :c 3})and for multi-arity functions, there’s no special syntax. It’s still just plain functions and forms
(def foo ([a] (print "I only have one" a) ([a b] (print "I have two" a b)))It also has more advanced ways of polymorphysm with methods and protocols.
It depends on the design of the language.








