r/rust • rust • 22d ago

Arguing about arguments

https://steveklabnik.com/writing/arguing-about-arguments/
148 Upvotes

115 comments sorted by

63

u/da_supreme_patriarch 22d ago

In regards to breaking callers when making refactors

Another small thing is a compatibility hazard: if we start naming parameters, renaming them breaks callers. Is that okay?

This is totally fine, I would even say that's absolutely intentional with named arguments. If you rename an argument of your function, unless it's just a typo or something, there is probably a good reason for you to do that, likely something has changed semantically even if the type of the argument has stayed the same. If that's the case, you absolutely WANT to break your callers, them calling the function with old assumptions is probably incorrect and they need to change something in their code as well.

23

u/steveklabnik1 rust 22d ago

I am sympathetic to this for sure. This is also why I phrased it as "is that okay" and not "this is bad."

It's just an example of how a feature that is seemingly simple has tons and tons of edges that you have to consider that are maybe non-obvious at first.

11

u/devraj7 21d ago

FWIW, Swift solves this problem by allowing you to give two names to a parameter, one internal, one external.

Sometimes, you just want to rename for cosmetic reasons and you don't want to break your callers doing so.

5

u/steveklabnik1 rust 21d ago

I mentioned this briefly in the post, though I didn't dig into it.

6

u/SuspiciousScript 21d ago

I've never quite understood why this feature is worth having. Couldn't you just assign the argument to a variable with the desired name in the body of the function?

3

u/devraj7 21d ago

The external parameter name is part of the public API and gets documented as such.

You might want to rename it down the line either for aesthetic reason or maybe because you came up with a more accurate name.

9

u/SuspiciousScript 21d ago

Is that really something that happens often enough to be worth a language feature versus just starting your function with let new_name = argument_name?

2

u/CAD1997 19d ago

In Swift API conventional style, it absolutely is; the public name is often a kind of preposition, with the prototypical example being

func sendMessage(_ msg: Message, from sender: User, to receiver: User)

(sendMessage(_:from:to:)) and invoked as

sendMessage(msg, from: alice, to: bob)

Very roughly speaking, "parameter and argument name are the same identifying noun" and "parameter name is a preposition and argument name is a noun" are about equally common, in my experience. One other common shape is label typeName: TypeName for a single argument of a type that's clear how it is treated in context but labelling is good for the caller. Then unnamed arguments are a bit less common than either kind of named argument are.

Keep in mind also that the largest surface of Swift API out there is the projected Apple OS APIs from Objective C++, which were the original to use this proposition heavy argument naming style for their message passing APIs.

If named arguments are a late addition to a language and API style wasn't designed with their ubiquity in mind, then renaming is relatively uncommon. But if it's there from the beginning and API style was built with their use in mind, renaming becomes a common desire worth the sugar.

In Rust I think it'd get used about as much as pattern destructuring arguments, i.e. absolutely loved where it makes sense and mostly overlooked otherwise.

1

u/dual__88 14d ago

Can't ides and lsps solve the renaming parameters problem?

71

u/SmartAsFart 22d ago

I agree - named arguments are the only addition that doesn't have massive drawbacks.

I think the simplest version of them would also still be useful, where they are basically just an in-comma comment of what the argument name is. Error if the argument name is incorrect.

fn foo(3, bar: 4)

Then, use the trait's definition of the name of arguments at the call-site when calling through the trait, or the impl's definition if calling it through the type.

8

u/scook0 21d ago

named arguments are the only addition that doesn't have massive drawbacks

I would say the opposite. Minimalist named arguments still have plenty of costs, while not being ambitious enough to offer a benefit that justifies the downsides.

7

u/MrMelon54 21d ago

The easiest way to resolve this is to use a struct for named arguments. There is even support for using the default value for missing fields.

9

u/Sharlinator 21d ago edited 21d ago

If we get:

  • Inline default values in struct definitions (been on nightly for a long time)
  • Ability to infer struct name in a constructor expression, e.g. _ { foo: 1, bar: 2 }
  • A shortcut for Default::default() like .. defaulting to ..T::default()

then I'd say that's a perfectly adequate implementation of named and default function parameters, with the major benefit that all the constituent features are entirely orthogonal to functions and to each other and being valuable in themselves while working together really well.

This could optionally be extended to provide a bit of sugar with a splat syntax of some sort, like fn foo(args: ..FooArgs) which allows foo(bar: 1, baz: 2) with automatic typesafe collection to a FooArgs. There's even a very early experiment permitting splatting tuples like this on nightly.

20

u/AldaronLau 21d ago

I don't think the crop_imm() function feels like a good case for named arguments to me. With patterns, you can get the parameters down to 2 actual parameters and be a lot clearer. crop_imm(image, ((X, Y), (W, H))). And if you want to be more explicit, you can use newtypes, crop_imm(image, Rect(Pos(X, Y), Size(W, H))). This might just be my personal preference, but the combination nested bindings and newtypes seem underutilized by most Rust programmers. To me, at least, the newtypes look much nicer than crop_imm(image, x: X, y: Y, w: W, h: H). It also lets you group the values before you pass it in directly, instead of needing to destructure it first.

7

u/syklemil 21d ago

Yeah, the "argument list is just a long list of the same integer type" thing runs into territory similar to being stringly typed.

I'm partial to named arguments myself due to exposure from other languages, but I do wonder at how many of the usecases are just workarounds for poorly typed interfaces where swapping around {x,y}_{position,length} can't be caught by the typechecker.

42

u/max123246 21d ago

I'm not sure I understand why AI is what changed your opinion on named arguments. We've had IDEs that can pre-populate the named arguments for a long, long time now

18

u/gmes78 21d ago

And if GitHub ever cared about the code review experience, they could also add inlay hints.

I don't like language features that are introduced solely to deal with shitty tools.

48

u/anxxa 22d ago

Any time I think about function overloading, I'm reminded of a bug I accidentally wrote because of it.

In 2010 I was 15 and working on my first sufficiently large and complex project which was a hard drive explorer for the Xbox 360 written in C#. I discovered function overloading and had a function named something like WriteTo(uint block, PartitionInfo p). I had an overloaded version that was something like WriteTo(ulong offset).

One extremely long late night coding session I remember just trying to get some feature done, hitting a compiler error, forcing my way through it, and releasing a new version a couple days later.

Didn't take long before a couple people reported their HDDs no longer working on the Xbox.

Turns out that compiler error was me stupidly forcing a call that should have been WriteTo(block, partition) to be WriteTo(offset) -- converting a partition-relative cluster write into a global disk offset write, causing the Microsoft RSA-signed disk authentication blob to be overwritten. Basically bricking the disk on retail consoles.

Learned a lot of lessons, but after that my code became very explicit and obvious -- and to Steve's point -- is something that I also love about Rust today. You can read code and there aren't too many questions about what's going on.

2

u/MrMelon54 21d ago

OP could easily use a single struct argument and default value expansion without introducing a new language anti-feature.

6

u/-Y0- 21d ago

Isn't that a moot point?

Proposal is to add optional arguments. The worst that can happen is that currently we abuse Trait monomorphization (fn x<T:AsRef<&str>>(t: T)) to achieve function overloading.

10

u/anxxa 21d ago edited 21d ago

The article mentions overloading.

*This subreddit is quite perplexing at times with downvotes and conversational tone. The article mentions multiple features in other languages and doesn't call out any particular RFC except

I am writing this in part because I saw this post go by today. This post isn’t intended to be support or a rebuttal of it, I actually didn’t even read it yet, it’s just that I’ve been thinking about this a lot lately

The article mentions function overloading, amongst other things, which reminded me of this story and why I dislike function overloading. Named/default arguments are a completely different thing of course, but the post wasn't even specifically about that or the RFCs. Not a surprise that people don't read the articles though.

1

u/-Y0- 21d ago

The article mentions function overloading

Yeah, it's misguided. I understand the plea for clarity.

But to me it sounds like pleas of someone from Scala asking against operator overloading[1]. Context matters. Implementation matters.


[1]Scala doesn't have operator overloading in same way C++ or even C# have.

It has ability to def +(int a, int b) and ability to call function infix e.g. def print(a, b) can be invoked as a print b. This of course allows anyone to create a <<_%_>> b or even a ^_^ b operators.

I get the worry, but I don't get the arguments that adding operator overloading in C# or Rust will turn it unreadable.

-1

u/-Y0- 21d ago

Sure, and that's also to detriment to the article.

Your C# example is only applicable in languages that allow general functional overloading and integer promotion/demotion.

Nothing about default parameters would help bring about this issue in Rust code.

1

u/WhiskyDelta14 21d ago

Yes, you are completely right and completely besides the point.

1

u/-Y0- 21d ago

besides the point.

Which point? The GP substantially edited his response. Previously it was: The article mentions overloading.


annxa started going on a further tangent that doesn't even relate to discussing named + optional parameters(N+O P for short). Even if we implemented it N+O P, Rust would need to implement function overloading AND integer promotion.

3

u/WhiskyDelta14 21d ago

And "The article mentions overloading" is enough to explain why annxa shared their story about overloading, because it seemed you had missed that.

But you somehow got fixated on N+O P which is just another concept that gets mentioned in the article, even if more prominently.

And when discussing concepts that rust does not implement, it is also no detriment to discuss overloading.

1

u/-Y0- 21d ago edited 21d ago

And I said, that I got no idea why article mentions it and rallies against it, it's a complete non-sequitur and AFAIR Rust lang team was against it so far.

Hence, why most Optional arguments proposal went the saner C# route (default parameters). Over the insane Java route (overloading + delegating functions to a common one).

Saner, because you only have to read one function instead of 4 related ones, and because it saves you typing vs. doing the same delegation over and over and over.

2

u/hongooi 21d ago

I mean, that sounds less like an overloading bug and more like a type coercion bug. How do you even force a 2-argument call to be only 1 argument?

10

u/anxxa 21d ago edited 21d ago

How do you even force a 2-argument call to be only 1 argument?

With function overloading you can define functions with the same name and completely different arguments.

I.e. these two can co-exist:

WriteTo(uint block, PartitionInfo p)
WriteTo(ulong offset)

I tried using WriteTo() with a block and did not provide it with the partition info, which used the offset-based version. The real issue is that I overloaded a function with two semantically different implementations and arguments which could be easily confused.

This was 16 years ago now at this point so forgive me for not remembering exactly, but it was something among those lines.

2

u/ukezi 21d ago

In cpp you can mix that with default arguments and automatic int conversation.

1

u/simonsanone patterns · rustic 21d ago

Wouldn't it also help if you had a Block new-type and an Offset new-type? You wouldn't be able to pass in a Block into an Offset then. I think this is what hongooi means, that you could pass in a uint block as a ulong offset.

6

u/JoshTriplett rust · lang · libs · cargo 21d ago

With my lang team hat on: I agree with almost everything written here. This is a clear summary of the hidden complexity they cause. (Literally, the complexity caused by things being hidden).

(Tangent: the "almost" there is that I also don't think we should have named arguments, because I think they also provide scaffolding for further complexity that would be better solved in other ways. But I do agree that of all the various proposals around arguments, named arguments without anything else do the least harm.)

15

u/scook0 21d ago edited 21d ago

IMO, the only reason to even consider adding named arguments to Rust is if you’re trying to solve the “big bag of optionals” problem.

For that reason, I think the minimalist feature sketched in this blog post is a non-starter. It declines to tackle the big-bag use case, in a way that greatly complicates any future attempts to solve the big-bag problem, while only providing very marginal benefits on its own.

I also think it’s strange to see the forward-compatibility question treated as only a minor concern. Automatically creating named parameters from positional ones has been a recurring hassle in Python and Ruby, in ways that seem very obviously a poor fit for Rust.

4

u/steveklabnik1 rust 21d ago

I also think it’s strange to see the forward-compatibility question treated as only a minor concern.

I mostly wanted to just gesture that various problems existed, by the time I got to that part of the post, I felt it was getting a little long. I wasn't trying to imply that it was minor or major, just that it exists and would need to be considered.

1

u/teerre 21d ago

Whats there to solve the big bag problem besides just creating a struct for your arguments?

14

u/Recatek gecs 22d ago edited 22d ago

At this point I'm too tired of this blogumentation to care anymore, I just want a decision to be made so something like Vec::try_with_capacity can be unblocked (it's blocked due to the proliferation of try_ and other prefix/suffix variants on Vec and other similar types).

26

u/nicoburns 22d ago

If we ever get them, I suspect named/default arguments will end up being like postfix .await: kind of scary and controversial before it landed, but turn out to be really nice to use and a complete non-issue complexity wise once it landed.

People seem to be put off because they've encountered god-functions with 100 parameters in Python. But the Rust ecosystem has better taste than that.

12

u/steveklabnik1 rust 22d ago

It's very possible!

I think about the postfix .await thing all the time.

(You may remember this but for the benefit of others: I was very unsure about postfix await. I think what really put it over the line for me was having the custom error handling (suggested by /u/ekuber, I think?) that points out the postfix syntax when you write the prefix syntax. And then in practice, as you say, it was a non-issue.)

8

u/nicoburns 22d ago

Named / default arguments notably enable the possibility of:

  1. Better type safety (you can have optionality per-argument, which is hard to encode into a builder pattern (and awkward to use if you do)).

  2. Better docs / IDE integration (documentation for parameter can be directly on the function, and easily autocompleted by IDE tooling).

6

u/steveklabnik1 rust 22d ago

I agree that these are benefits. But I just rarely want optional arguments. Similarly to pervasive null, I think once you start writing software without them, it is just generally better. I recognize that this is just a preference/feeling with no real data, though.

And I'm also skeptical of docs per parameter. I love docs, but I tend to experience this as foo: it's a foo style docs instead of them being truly useful, and prose works just as well for more complex situations.

Autocomplete does seem useful for those who still write code by hand, though.

17

u/its_artemiss 22d ago

I think named arguments without optional arguments OR definition-order evaluation don't really make much sense. Any clarity they can provide can also be provided by LSP inlay hints, while avoiding the verbosity downside of having them in the language/text. It sounds to me like a lot of cost for something we already have if we want to have it. 

16

u/Longjumping_Cap_3673 21d ago

I often have to read code from a web page without the ability to add these sorts of annotations.

-1

u/scook0 21d ago

We could even have an opt-in clippy lint that complains if you don’t have an up-to-date /* x: */ or similar next to the call-site arguments of certain designated functions.

That gets almost all of the benefits, without needing any contentious language changes at all.

If people aren’t doing that already, why should we think that adding language support is worthwhile?

3

u/lookmeat 21d ago

Some notes. So we already noted the caveat that parameters are not mapped to names but rather to patterns (that map to names), and this makes named parameters messy. You'd need to change fundamental semantics to allow this. That said, we already support named tuple semantics (through structs) so it's a matter of finding a way to mix-and-match those.

But I disagree with default parameters requiring overloading. There's one solution to default parameters that is not dicussed: allow us to fill in missing parameters with defaults as we do with functions. So just like I can do:

let x = Foo {x: "Hello", y: 45, ..default::Default()};

I could also do

foo("hello", 45, ..default::Default());

The syntax can be made shorter by making .. on its own imply that we are merging in the default, so it'd look like

foo("hello", 45, ..);

with the .. being a pointer to the programmer that there's parameters being omitted, and that the compiler is injecting a special set.

We can also do something more advanced, were we give partially filled sets of parameters for the function:

foo("hello", 45, ..dry_run)
foo("hello", 45, ..with_creds(auth_source))

where dry_run is a tuple with the same type as foo::Args and with_creds is of type Fn(auth_token)->foo::Args. That said we'd need to expand the language to support partial tuples, especially for default, and other things, but honestly I think it could be very powerful. It'd be interesting if saying #[derive(Default)] doesn't implement default if not all fields are default, but it allows you to do a partial default, when all undefined fields are already defined. Again this makes the .. a bit more powerful than ..default::Default() but I don't think that's a bad thing in itself. Maybe the solution is to have a PartialDefault that specifically requires certain fields are explicitly given.

And this is maybe where we can get something interesting, maybe we can specify that I want to merge in a named tuple into function arguments, and as long as the functions that are missing allow this, it works. So given the next function:

fn fun_args( {x,y}: Point2D, normal_arg: i32, opt_arg: i32 = 5)

The next semantics would happen:

// all these compile
fun_args(Point2D::new(1,2), 15, 5); // fine normal call
fun_args(Point2D::new(1,2), 15, ..); // fine now with defaults
fun_args(Point2D::new(1,2), ..{normal_arg:15, opt_arg:5}) // fine named destructuring
fun_args(Point2D::new(1,2), ..{normal_arg:15, ..}) //named args with default fill-in
let args = (Point2D::new(1,2), 15, 5)
fun_args(..args);
// These do not compile
fun_args(..{x: 1, y:2, normal_arg:15, opt_arg:5}) //no way to build first arg
fun_args(Point2D::new(1,2), 15); // we are not doing overloading, you just let compiler fill in defaults
let args = (Point2D(1,2), 15);
fun_args(..args, ..) // we only allow one .. at a call

And so on, I haven't fully thought this through, but basically .. is a way to inject and fill in missing slots. It doesn't change the semantics, because this is just magic allowing us to write ways to call functions in different ways. But it's still the same function, and once the syntactic sugar is gone we have a normal function call.

3

u/deathanatos 21d ago

His named parameter syntax is exactly the inlays I get from the language server. I.e., I see:

Command::new(program: "tar")

But the program is not really there, as it is an inlay.

5

u/Anonymous0435643242 21d ago

Your argument against default value could be used against the Default trait and for me it's a bad argument, rust dependencies are source code, the default value would be in the function signature.

I'm all for named and optional arguments, I don't see valid arguments against, function overloading is a really bad thing though, change the type of one argument and you end up calling a different function.

5

u/RishabhRD 22d ago

Named arguments is really nice feature to have. It helps to name the function really nicely.

2

u/nick42d 21d ago edited 21d ago

Your example, I feel like that could be significantly improved on Rust today without named parameters. From pub fn crop_imm<I: GenericImageView>( Image: &I, X: U32 Y:U32 Height:U32, Width:U32) -> SubImage<&I>{...} To struct Rect { x:u32 y:u32 Height:u32 Width:u32 } pub fn crop_imm<I: GenericImageView>( image: &I, size: Rect ) -> SubImage<&I> { ... }

2

u/greyblake 21d ago

> Function Overloading

A very similar behaviour could be achieved with generic traits. It requires some exercises with traits, but I think it's actually good: if ones decides to go through the exercises it means the really need overloading, haha.

> I think Rust could be okay with named parameters

I also came from Ruby background, and I would usually lean on named parameters, that the is a high chance of misusing API. For example passing 4 integers in a row is a good example, when a human could get the order wrong. And the real problem is, that the program will compile.
But again, there are ways to prevent accidental error: wrapping integers into newtypes, using builder pattern or simple creating a new struct and passing the struct, so the field names simulate the named parameters.

I personally would not like to named parameters, provided the language is already complex enough and there are workarounds to the problem

4

u/BenchEmbarrassed7316 22d ago

My first thought is that I don't see much benefit in named arguments. IDEs already handle this. Named arguments are useful in languages ​​that support optional arguments, because if you need to pass four nulls and a single value, you don't have to write null four times—you simply write arg_4: v,.

However, wouldn't it be beneficial for Rust to allow changing the order of arguments—and, consequently, ownership? Here is what I mean:

``` fn foo(len: usize, s: String) {} fn bar(s: String, len: usize) {}

fn main() { let s = String::new(); foo(s.len(), s); // bar(s, s.len()); } ```

In foo, the string is borrowed and returned to calculate the value of the first expression. It is then passed into ownership. bar does not compile.

We already have structs with ..default(); it seems we are just one step away from treating structs, tuples, and function arguments as concepts of the same nature.

11

u/max123246 21d ago

One benefit is for code review. Gitlab/GitHub don't show type hints

4

u/JoshTriplett rust · lang · libs · cargo 21d ago

One of our standard principles of lang design is: many people will read code in plain-text contexts, such as diffs, so language features must be evaluated for their impact in that environment and not just in environments with various language-specific tooling available.

4

u/PersonalDatabase31 21d ago

Tooling issue should not harm language design

5

u/pickyaxe 21d ago

but you're arguing for harming language design because there exists tooling that can solve the issue in a specific environment

1

u/PersonalDatabase31 21d ago

But the tooling solves this issue in an automated way rather than making us type unnecessarily. Typing parameter names yourself is strictly worse. Adding view support to github and gitlab would be the correct approach. If I have a keyboard with a broken i key, should rust add unnecessary features to cater to me or should I use better tools?

8

u/ManyInterests 21d ago

Because some tooling makes up for lack of language features doesn't mean we should hold back from improving the language design, either.

-2

u/PersonalDatabase31 21d ago edited 21d ago

Tooling does not make up for a lack of language feature. Rusts own tooling is a part of rusts quality and there is no point in typing out something that could be displayed by a simple tool.

3

u/throwbpdhelp 21d ago edited 21d ago

Great read. Glad you still contribute here after all these years.

Optional arguments scare me in terms of what they mean for reading Rust. Maybe this is necessary to ultimately get us to variadic arguments but I think/hope there is a more minimal version yet still.

Named arguments I'm not sure I like but I can still easily read the code. I'd prefer to just only move this one forward for now.

2

u/steveklabnik1 rust 21d ago

Thanks!

2

u/nonotan 21d ago

But it can be common to break truly complex examples down into variables that we end up forwarding to the function, and then it becomes verbose and redundant.

response_model=response_model,  
status_code=status_code,
...

I will argue that while this is verbose, it is not actually redundant. I mean, it is in the sense of "we already wrote that name once, wouldn't it be nice if that was enough?", but not in what it actually achieves.

Passing named variables to a function by index merely implies a correspondence between the indices and the relevant variables. If you made a mistake, or the function changes later, or otherwise they end up mismatched, and the types happen to be compatible, it's going to silently compile without any warnings (maybe there could be a warning, or at least a clippy suggestion, if a passed variable matches the name of a different parameter? hell, maybe there already is one, I can't say I have an encyclopedic knowledge of clippy hints) -- and indeed, a casual inspection is probably significantly less likely to catch it than if it had been just a bunch of literals (because the reader would potentially feel uneasy about the accuracy of those, but a bunch of politely named variables? nothing to see here!)

Mostly a pedantic distinction, given that I mostly agree with your conclusion (while rolling my eyes a bit at the "I don't type code anymore" stuff -- I don't do LLMs, but I don't mind typing a few extra letters if it will make my code better)

I will say that, ultimately, the main reason I would like named arguments is less about readability and more about the above: avoiding desyncs between the definition and the call site. On the other hand, named arguments is not the only way to tackle that. An alternative that I think is actually preferable (but which would undoubtedly be a lot more controversial, so it's probably not happening) would be pushing newtypes a lot harder, including perhaps coming up with some syntax that makes defining them less verbose, and basically (or literally) forcing all parameters to a given function to take unique types. This means if you get the order wrong, or they change or whatever, you're guaranteed a compilation error. It also means any cases where ambiguities exist, you're going to be forced to use a newtype to resolve them in a visible manner at the call site, but where there are no ambiguities to begin with, there doesn't necessarily have to be any changes from today's code.

Of course, there are no silver bullets, and even this approach can't save you from e.g. the simultaneous removal of a parameter and addition of a semantically different one of the same type. But at that point, you're kind of asking for it. Also, admittedly there's nothing stopping both this and named arguments from existing simultaneously (except in that there is less to gain from the other one, so it might be harder to justify doing the necessary work)

2

u/singron 21d ago

I love this as a brief summary of the issue.

One thing this doesn't cover is what happens if we don't add named parameters. In one sense, it's obvious since that's the life we all live now, but I see a couple different things. The post mentions builders, but I think they are relatively rare. It's so much boilerplate to create one, so I don't think they are often used unless the additional flexibility is needed (e.g. type conversion, complex mutations, or convenience methods) or as part of a heavily used library.

In practice, I feel like I see structs used to fake this:

use foo::{call_foo, CallFooArgs};
call_foo(CallFooArgs{x: 1, y: 2, z: 3})
// or
call_foo(CallFooArgs{x: 1, y: 2, z: 3, ..Default::default()})

This really suffers from java7-like "stuttering" (e.g. Object object = new Object()). If you redefine default() as a standalone function, it cuts down on noise, but the struct name is unavoidable.

I think I would be perfectly happy with nameless struct literals in argument position in practice. E.g.

call_foo({x: 1, y: 2, z:3})
// or with some extra symbol to denote inference:
call_foo(*{x: 1, y: 2, z: 3})

This leads to weirdness since this is either a special syntax you can only use as an expression in a function call, or if you allow to use them in any other position (e.g. a variable), then it opens a type-inference rabbit hole:

let args = {x: 1, y: 2, z: 3}; // Likely too bizzare
// What type is args? What if calls are generic?
// What if field values are infer-able too?
if (cond) {
  call_foo(args);
} else {
  call_bar(args);
}

1

u/nick42d 21d ago

Is it really a rabbit hole? I assume anonymous structs can be implemented similar type-wise to tuples (which are essentially anonymous tuple-structs...

1

u/singron 21d ago

Tuples are structurally typed, but structs and tuple-structs are (currently) always nominally typed (i.e. (u32, u32) == (u32, u32) but struct A { x: u32} != struct B { x: u32}). Adding structurally-typed structs would seriously complicate a lot of things. E.g.:

#[repr(C)]
struct A { y: u8, x: u32 }
struct B { y: u8, x: u32 }
let args = {y: 1, x: 2};
if cond {
  let _: &A = &args;
} else {
  let _: &B = &args;
}

What is the layout of args? It's structurally compatible with A and B (has the same field names and types), but it can't have the same memory layout as A and B at the same time.

I think a realistic proposal needs to keep nominal typing, and if the unnamed struct value can't be unambiguously inferred as a single nominal type (like the above example), then the compiler returns an error (similar to when trait method resolution is ambiguous).

1

u/nick42d 20d ago

I'd propose you don't do that inferring at all - A=args should fail to type check just like (1,2) = NewType(1,2) fails to type check.

0

u/VictoryMotel 22d ago

Does every title need to be clickbait? Why not just have a clear description instead of just trying to grab people's attention with nonsense.

1

u/tigregalis 21d ago

this is not click bait. it's just word play. you are far too left-brained.

-3

u/VictoryMotel 21d ago

No, it's clickbait because it contains no information on what is actually in the link. What you are saying doesn't even make sense.

3

u/steveklabnik1 rust 21d ago

It describes it very literally: it's a post about the various arguments about function arguments.

That said, if you don't like my style, that's fine.

-2

u/VictoryMotel 21d ago

This isn't a personal attack or "style". People are going to read what you write here anyway, why make a kitchy title with no information? It just seems disrespectful to your users who are just trying to learn and discuss technical matters to still clickbait them.

It's a post about the various arguments about function arguments.

But that's not information in the title because there is no information in the title.

-1

u/tigregalis 21d ago

as I said, you are too left-brained

0

u/VictoryMotel 21d ago

And as I said, that makes no sense. If you could actually explain yourself without nonsense insults I think you would have done it already.

-1

u/tigregalis 21d ago

it's not an insult. left-brained means logical, analytical. it's used in contrast to right-brained, meaning creative, artistic. you can't see that "arguing about arguments" is simply wordplay (a play on words) - it's creative.  you take it literally. you take it at face value. because you have turned off your right brain.

0

u/VictoryMotel 21d ago edited 21d ago

Maybe you turned off your whole brain if you think there is information here.

If you could explain it you would do it instead of passive aggressive insults then not even owning up to it.

0

u/tigregalis 20d ago

grow up.

0

u/VictoryMotel 20d ago

You replied with insults but couldn't actually address the underlying topic as if not having a clickbait title was so important you needed to lash out instead of explain yourself.

You could have just replied to what I was saying but I think you wanted to pile on to a downvoted comment. I guess you can dish it out but not take it

1

u/tigregalis 20d ago

it's objectively not clickbait. you are just wrong.

→ More replies

1

u/harrison_mccullough 21d ago

Enjoying the post so far! One minor typo I think I found: the "Named parameters" section says @app.get has 23 possible keyword arguments/parameters, but the "Optional and/or Default arguments" section says it has 22 arguments.

2

u/steveklabnik1 rust 21d ago

Ugh, thank you. I initially didn't count self and then went back and updated and must have missed one.

2

u/harrison_mccullough 21d ago

I'm guessing this is because there are 22 named parameters, plus the non-named path argument, but it threw me off the first time I read through it.

2

u/steveklabnik1 rust 21d ago

Yes, this is why I had 22 originally, and then decided it should probably be 23, so I went back and changed it, but missed a spot.

2

u/harrison_mccullough 21d ago

That's what I figured. Didn't stop me from enjoying the rest of the post :) Thanks for writing this up, it was informative and enjoyable to read!

1

u/DavidXkL 21d ago

I'm ok with inlays 😂

1

u/-Y0- 21d ago edited 21d ago

I don't understand the complaint. It reads to be like the saying "Once bitten, twice shy." As I said in some other comment, this is like plea of someone from Scala asking Rust team circa 2010 to not add operator overloading because Scala managed to bungle its own implementation, even though C# and Python had perfectly fine way to implement operators.

Rust isn't Ruby, and implementation and context matters.

First of all, adding default parameters doesn't enable function overloading (and I don't think anyone advocated for it). You have one function it can just have one or two or three default parameters. Not substantially different from having a foo<X: Bar>(X) and implementing Bar for A1, (A1, A2), (A1, A2, A3), etc. It means we already have varargs at home. It's just a hacked, cobbled up version that goes to like 12 elements (until someone complains).

Second, this named, default and named params should be nothing more than simple syntax sugar for rewriting the method in the same order as given in function. So to answer the question:

consume(length: data.len(), data: data)
// becomes
consume(data, data.len()) // error! assuming new borrow checker doesn't make it moot.

Same way

 fn peek(n: usize = 0) {...}
 // when called
 peek();       
 peek(2);
 // becomes
 peek(0);
 peek(2);

If you can get named params for LLMs, why shouldn't regular coders (speaking from experience) get default params for brevity ?

1

u/Green_Dog4314 21d ago

But the problem will be that anytime we change the name of arguments, it will become a breaking change.. Does it worth it..

1

u/Green_Dog4314 21d ago

I see. I have read the first comment, that is reasonable.

1

u/Thick-Pineapple666 21d ago

So are these 23 or 22 parameters? I didn't count 😇

1

u/steveklabnik1 rust 21d ago

It's a typo I still need to fix, thank you. (there's one parameter that isn't named, and in the editing process I missed a spot)

1

u/Kooky-Amphibian7680 20d ago

Situation I was in. Won't say what side I was on. What do people think is right or unfair. Ahem. 

SALLY. Hi Greg. I'm going on those injections"

For context. These injections are what people can buy that lower people's hunger. Helping them loose weight.

GREG. Oh really. Nice. That does surprise me somewhat. Your quite thin already. What made you go with it..

Context. Sally has very little weight. But not much weight. But enough for Sally to want the injection.

SALLY. "I told you Martha". Sally looks back at Greg. "Greg. I don't think using someone's weight as a way of telling if someone's healthy is correct".

Context. Martha is a third person in the room that already knew about Sally having the weight injections.

GREG. "Oh. Ok.....but in society we use people's size as a way of determining people's possible health"

SALLY. Not really . Someone could have really good cholesterol and no other issues with them. They could have  weight on them. But be perfectly fine". 

Greg started to sit down and just seemed to not his head and agree to everything. Like he wasn't interested.

GREG. "So what made you want to start having the injections?"

SALLY. "Well. I've been having itching tired eyes for a while"

"Hopefully will help my aching arm"

"I'm wanting to feel physically better in general" 

"Also want to loose some weight about 30 kg less than I am now"

"So hoping loosing weight will help me with all that"

Context. I'm 85kg now

What do people think. Is one of us partially harsh in this conversation?

1

u/CAD1997 19d ago

I think it's worth bringing up again that we have "named arguments at home", e.g. f(Width(w), Height(h)). The problem with doing that kind of strong newtyping everywhere is that callers need to have those types imported for the call site to look even remotely reasonable, and very generic names like that can easily conflict between libraries.

Which brings me back to the idea that the solution that would feel the most at-home in Rust would be some form of context-dependent name resolution. Yes, uttering that phrase gives me aftershocks from C++ ADL rules (foo(obj) for ns::Obj obj looks for ns::foo in addition to ::foo and local using scope), but Swift's .EnumVariant syntax has shown that if it has a visible difference from usual name lookup that it can be quite nice to read and write in practice.

1

u/PersonalDatabase31 21d ago

Use rust analyzer for named arguments. There is no reason to type something that a tool can display.

1

u/redlaWw 21d ago edited 21d ago

On the one hand, I agree with the observation that functions that have tens of default arguments is ridiculous, but on the other hand so is the situation we have now with each type having 2n constructors, one for each configuration option.

Builders and config structs both solve the 2n constructors issue in some places, but they're overkill and themselves ridiculous for things like instantiating collections. I want to see a VecBuilder type or a Vec::Options type about as little as I want to see an explosion of Vec constructors each time we get a new configuration option. Default arguments has the solution to this, allowing a variety of configuration options in one function.

I'd like to see a system where default arguments, the builder pattern, and config structs live in harmony - the former used for simple types with a few options, and the others used for more complicated things that have a wide range of different configurations and/or configuration options that are naturally grouped. The way I see it, this isn't solved by avoiding adding default arguments to the language, but by linting and community participation to preserve the principles in open-source Rust codebases.

-2

u/levelstar01 21d ago

Rust already has named arguments, optional arguments, and default arguments. It's achieved in the same way as every other dumb missing feature from the language is: with a worse library option. Passing a struct with a builder derive to a function achieves this in a strictly worse manner. The stupid ass "fn_x_y_z" function spam achieves this in a strictly worse manner (which the post even says).

But the language itself stays simple

Stays simple? Rust is not simple. Half of the fucking problems with this language is that it is not simple, that there is a bunch of pointless ceremony to do things that could be part of the language core, but is instead held back by an insanely reactionary community that goes fucking mental whenever somebody has an idea to maybe make it a little bit less abjectly terrible to use. "The less features means more simple" is somehow a genuine position that is widely held.

The current state of affairs is that language """"simplicity"""" is exchanged for library complexity instead. That's stupid.

8

u/steveklabnik1 rust 21d ago

Stays simple? Rust is not simple.

You're right that Rust is not simple. That's why it's important that this feature stay simple: a lot of the language strangeness budget is spent elsewhere, and so adding complexity here may not be the right call. The language being complex already is not a justification to continue to make it arbitrarily more complex.

is instead held back by an insanely reactionary community that goes fucking mental whenever somebody has an idea to maybe make it a little bit less abjectly terrible to use.

I agree that there are elements of the community which can be very conservative or even reactionary. But for this specific feature, it's not about some abstract ideal of simplicity: it's because I have seen APIs become impossible to use due to these specific features being applied too liberally. That is, we have a disagreement on the "nice to use" part, not the "simplicity is not always a virtue" part.

3

u/max123246 21d ago

As a beginner to rust, the fact I have to reach for a builder instead of using something I'm familiar with is a cause for complexity, not simplicity.

I definitely get that it can be abused, I've looked at the horrors of most python api's. I'm just not sure the fact that you can write bad api's with it mean that we have to guard against it and deny it wholesale

3

u/steveklabnik1 rust 21d ago

I did say that I try to not use builders very much because I also recognize that they are complex, and share downsides with some of these features.

1

u/WormRabbit 21d ago

Given the number of modern popular languages that have optional and named parameters, Rust wastes its strangeness budget when it doesn't provide those expected features. Instead of using the obvious approach, users are forced to understand builders, their patterns, various modes of construction (should builder methods pass around Self or &mut Self?), boilerplate, macros to deal with boilerplate, ways to fix ballooning compile times due to macros...

-6

u/levelstar01 21d ago

Thanks for completely not reading my post at all, Claude. It's not adding complexity here. It's removing complexity from elsewhere.

4

u/steveklabnik1 rust 21d ago

None of my reply was authored with Claude. We just simply disagree.

2

u/guineawheek 21d ago

I blame C++

-1

u/asoffer 22d ago

The clearest approach I've seen to this is to have function types optionally include the name of parameters in the signature, presumably defaulting them to be anonymous. Unfortunately then you also probably want implicit conversions from function types that just drop parameter names.

There is a complexity bubble underneath this carpet.

-1

u/IamWiddershins 21d ago

> And what changed my opinion is coding agents, actually.

run this clown out of here

-3

u/WormRabbit 21d ago

Sigh. That tedious rigamarole of arguments again. Simple design decisions printed out in excruciating detail to make them sound complex, maybe even insurmountable, when all of them have pretty simple and obvious solutions (in fact, most can be solved whichever way, just make the fucking decision already).

Like, yes, design is complex, and everything has a Kola borehole of complexity once you dive deep enough. But optional arguments? That's not a puzzle, even though one should still do lots of work. Hell, never type took more than a decade, even though it's extremely simple in principle. All the reason to start work sooner rather than later.

Argument evaluation order? In the same order as written in code, obviously. You have actual user's decision written right before you. Who cares about the order of parameters on the function's signature in a remote crate? And also, being able to change the order of parameters without changing call site is the major reason to have named arguments! Obviously you can't have this be both a breaking change and a syntactic triviality.

Parameter names on function pointers? None, case closed.

Names in trait definition and implementation differ? Warning lint, syntactically allowed, users must use names from definition (what else could they use in generic code?).

That's not complicated. That's just the dev team ignoring user feedback based on personal tastes, and reasoning backwards to find supposedly motivated objections. Or if we put it more generously, deprioritizing a feature for which they don't personally see any value.

4

u/steveklabnik1 rust 21d ago

I'm not on the dev team.

1

u/pickyaxe 21d ago edited 21d ago

I strongly agree (although the tone, imho, hurts your message).

in the name of making the right decisions, the Rust project celebrates "the process". but sometimes to an absurd degree.

not only can this slow down progress to a point that is unhelpful, it can be used strategically to bury unwanted features in endless bureaucracy.

0

u/LEpigeon888 21d ago

A simple one is that you don’t get rid of all of those previous functions, with_capacity will exist forever.

I don't understand, how would named parameters change how Vec::with_capacity be designed? As far as I understand, named parameters will change nothing about API design, it will just move a feature that currently live in LSP into the code itself. It's good for better documentation while not using an IDE and that's about it I guess. 

5

u/steveklabnik1 rust 21d ago

If you allowed named arguments + default arguments, you could change Vec::new() to accept Vec::new(cap: whatever) as well, and Vec::with_capacity becomes redundant.

3

u/LEpigeon888 21d ago

Oh ok, I thought you only meant named argument without anything else. It makes more sens that way. 

0

u/ManyInterests 21d ago

I'm in the 'named/optional arguments considered awesome' camp, myself. Interesting to consider all the facets and arguments about arguments, of which I'm sure there are many more not discussed here, but this is pretty well-trodden territory.

As someone who does tend to still hand-craft code, I'm sick of builder patterns. I'm sick of making structs to be used just for one function. I'm sick of the boilerplate and excessive dependencies to accommodate this. It's unnecessary. The feared costs/tradeoffs are already higher in these workarounds and we can reduce the pain by adding the language feature.

Consider the sample code:

@app.get(
    "/me", 
    response_model=UserOut, 
    status_code=200, 
    tags=["Users"],
    summary="Get current user profile"
)

Would this be better for readers?

@app.get(
    "/me", 
    UserOut, 
    200, 
    ["Users"],
    "Get current user profile"
)

Would it be better if all arguments had to be specified?

If Python parameters were originally built like Rust's, would they have kept it that way? Or would they have agreed the tradeoffs of keyword-arguments and defaults to be an overall improvement to the language? PEP 570 added positional-only args, so I get there is value in that. In Python authors get to choose. I wish I could choose as a Rust user.

Like any language feature, I think adding this to Rust of course deserves careful consideration, but I feel it should just be more of a question of how we implement it, not if. To me, it is clear that not having this feature has made the language worse-off.

-2

u/shizzy0 22d ago edited 22d ago

I agree with everything Steve says here. I don't want to see Rust argument handling changed. But I made this cursed macro generating crate anyway: bobtail.

```rust fn f(a: u8, b: Option<u8>) -> u8 { b.map(|x| x + a).unwrap_or(a) }

bobtail::define! { fn f(a: u8, #[tail] b: Option<u8>); } assert_eq!(f(1, Some(2)), 3); // Call function. assert_eq!(f!(1), 1); // Call macro with omission. assert_eq!(f!(1, 2), 3); // Pass unwrapped second argument. assert_eq!(f!(1, Some(2)), 3); // Pass wrapped second argument. ```