Raising the bar for introducing new syntax

I instead see the () vs {}, since parens mean positional to me – in both tuple structs and functions.

2 Likes

Fair enough! You're of course right that I've been treating {} like () by focusing on what's inside the braces (a comma-separated list).

I also don't like the opportunity for errors caused by accidental swaps, but I wonder how that is fundamentally different from initializing a tuple struct or even calling a function? Accidentally swapping two arguments can have the same fatal consequence in those constructs.

1 Like

Personally, for this reason alone, I think no language should allow more than 1 parameter of the same type for functions definitions and invocations UNLESS it supports and requires named parameters. This is a HUGE source of unintentional, and sometimes late discovered, bugs in software. I think a good rule would be that any function call that isn't a different type for each parameter (including implicit conversions) should require named parameter calling convention - both for readability and correctness assurance. If a method/function has more than 1 parameter of the same type, then, named parameter calling convention should be 100% required as follows:

fn foo ( a : i32 );
fn bar ( a : i32, b : &str );
fn foobar ( a: i32, b: &str, c: i32 );

foo ( 3 ); // OK
foo ( a : 3 ) // OK
bar ( 3, "Hello" ); // OK
bar( a : 3, b : "Hello" ); // OK
foobar( 3, "Hello", 5 ) // Compiler Error/Not Permitted
foorbar( a: 3, b : "Hello", c : 5 ) // OK 

Alternatively, if you don't want to allow "Named Parameter Calling Convention", then, just don't allow methods/functions to be defined with more than one parameter of the same type. That is, require the developer to create a struct to handle necessary values and only have one parameter (other than self) to cover all the conflicts. That seems counter-productive though. Better just to support (and require) named parameter calling convention IMHO.

For example, I might define a function\method like this:

fn add_point( self : &MyStruct, x, y : i32 ) -> ();

Today, that would be called like this:

s.add_point( 3, 5 );

Now, being that "Points" have a clearly accepted ordering for x/y, this seems clear enough, right? But, what if the create author for "point" did it the other way around? Are you sure you got it right? I know, I know, unlikely scenario for something as simple and well-defined as "Point", but, that's the simplest, least likely thing like this to have this problem. Many things are much, much, much more ambiguous. How about this?

fn frobnicate( self : &MyType, a : i32, b : &str, c : f64, d : f64, e : i32, f : i32, g : &str ) -> ();
...
s.frobnicate( 3, "Hello", 3.14, 7.67, 5, 7, "What did you do? You broke it!" );

Did I call that right? Are the parameters lining up correctly. Should I have swapped 5 & 7? Shoot, I have to double-check the docs. It will compile, but, might not WORK CORRECTLY.

Now this:

s.frobnicate( a : 3, b : "Hello", c : 3.14, d : 7.67, e : 5, f : 7, g : "Good-Bye" );

Would be much easier for me to spot that I've called the function correctly. Not only that, but, this would also work and be easy to spot that it is or isn't correct:

s.frobnicate( c : 3.14, d : 7.67, a : 3, b : "Hello", e : 5, f : 7, g : "EPIC FAIL!" );

This is even more apparent if the parameters have good names:

fn frobnicate( self : &MyType, 
               default_value : i32, 
               greeting : &str, 
               min : f64, 
               max : f64, 
               warn_min : i32, 
               warn_max : i32,
               error_msg: &str ) -> ();

Which, when called under these requirements looks like this:

s.frobnicate( default_value : 6, 
              greeting : "Hello There", 
              min : 3.05, 
              max : 9.62, 
              warn_min : 4, 
              warn_max : 8,
              error_msg: "YOU HAVE JUST MADE A FATAL MISTAKE! I HOPE YOU KNOW SOMETHING ABOUT HAND-TO-HAND COMBAT!"  );

or even this (order doesn't matter):

s.frobnicate( default_value : 6, 
              min : 3.05, 
              max : 9.62, 
              warn_min : 4, 
              warn_max : 8,
              greeting : "Hello There", 
              error_msg: "YOU HAVE JUST MADE A FATAL MISTAKE! I HOPE YOU KNOW SOMETHING ABOUT HAND-TO-HAND COMBAT!"  );

Wouldn't you rather READ the above than this?:

s.frobnicate( 6, 
              "Hello There", 
              3.05, 
              9.62, 
              4, 
              8,
              "YOU HAVE JUST MADE A FATAL MISTAKE! I HOPE YOU KNOW SOMETHING ABOUT HAND-TO-HAND COMBAT!"  ) ;

I really can't understand how anyone would ever prefer positional syntax for something like this. It's error-prone, difficult to read, and requires substantially more mental gymnastics to keep straight. I HATE code like this. Unfortunately, you come across it all the time. Why not just make it not allowed?

1 Like

I think you're overstating your case.

  1. This is too aggressive because there are symmetric things like mem::swap where two of the same parameter type are fine.
  2. This is insufficient because making all the types different doesn't improve things:
s.frobnicate(
    6_usize, 
    b"Hello There", 
    3.05_f32, 
    9.62_f64, 
    4_i8, 
    8_u8,
    "YOU HAVE JUST MADE A FATAL MISTAKE! I HOPE YOU KNOW SOMETHING ABOUT HAND-TO-HAND COMBAT!");

I think it's far more interesting to look at why people would write a thing like that, and what would make the "better" solution easier, rather than just trying to ban it. (Though you could certainly propose clippy lints to nudge people away from known-bad things, like methods taking 5 bools.)

For example, I'd like FRU to work on [non_exhaustive] structs, because if a function takes 7 arguments today it'll probably need 8 tomorrow, and it'd be nice to not always need to make a builder type for it.

1 Like

For something like that, a keyword on the fn definition could allow non-named-parameter style where it would otherwise not be permitted, forcing the API designer to think about those implications. For example:

fn mem::swap<T>( a : &T[], b : &T[] ) -> (); 

would be required to be called as:

mem::swap( a : my_a, b : my_b );

If the API author felt, that in this instance, non-named-parameter calling was appropriate and unambiguous, then, the declaration could be (strawman example, would require bikeshedding):

fn_sym mem::swap<T>( a : &T[], b : &T[] ) -> (); // fn_sym = "function, symmetric" which means that the function parameters order doesn't really matter as long as you have the same types.

which could then be called like:

mem::swap( my_a, my_b );

As it is today. This could be introduced first as a WARNING Lint, and then in the next Edition it would become a compiler error with "rustfix" supporting auto-upgrade of old code to the named-parameter style automagically.

It doesn't improve the readability much, but, it does improve the unintended errors. That being said, I agree, the rule I proposed is insufficient. I don't agree that it is too aggressive though (as I stated in the previous reply). A better rule might be, "Any method/function with more than 1 parameter (other than self) of the same type, or, with more than 3 (or 2?) parameters other than self)".

The rule sounds too complicated to me to have an intuition as a programmer about;

I would instead advocate a culture of using newtypes (such as for metres, time, etc…) to mark semantic intent. In addition, I think use of too many parameters can be a sign that you need to split data up.

3 Likes

Why would intuition matter here? If I try to call a function/method using positional notation that requires, under the rule, named notation, then, the compiler will just generate an error like, "You must name all the parameters for this method/function because....to call it. ....and then it would show the correct call....". Rust-Fix (or some similar tool, like your IDE) could potentially allow for automatically fixing it as well. For these reason, I don't think intuition would ever be a relevant factor.

Intuition matters because I don't want to get errors constantly from the compiler unless I can eventually train my brain's internal type checker to recognize things and fix them before the error even happens.

1 Like

Wouldn’t the default intuition be: If I’m calling a function with more than 2 parameters not of the same type, I should name them? That would work like 99.99% of the time (my guesstimate :slight_smile:) even if it didn’t exactly match the proposed rule. In C#, where one can call any method/function with named parameters at your choosing, I always do that for any function with more than 3 parameters. The code just reads so much better. Personally, I wish it were just a requirement. I think if Rust had such a requirement in the language, two things would happen:

  1. People would avoid creating crappy API’s like my example above
  2. When people did create such crappy API’s, everyone would call them using the nice, unambiguous named-parameter syntax and even though the API was crappy, it would be readable in client code

Again, this is largely opinion, but, I believe I’ve provided some compelling arguments and examples for the “Righteousness of the Cause!” :wink:

I 100% agree with this notion, but, I don't think it alone solves the problem:

fn predict_mixture ( substance_a : SubstanceEnum,
                     substance_b : SubstanceEnum,
                     amount_a : Liter,
                     amount_b : Liter ) -> ( CompdoundEnum, Liter )

Which reads better?

let ( compound, amt ) = predict_mixture ( substance_a : HYDROGEN,
                                          substance_b : OXYGEN,
                                          amount_a : 0.00003,
                                          amount_b : 0.00025 );

OR

let ( compound, amt ) = predict_mixture ( HYDROGEN,
                                          OXYGEN,
                                          0.00003,
                                          0.00025 );

I would argue the former (especially for code you didn't write and/or you are not 100% familiar with the API under use).

1 Like

If you said "heuristic" I might agree, but I'm not a fan of making anything here a "rule".

There are way too many ways to write a crappy API for banning them to be effective. For one, I can bypass everything you just said by putting all my parameters in a tuple instead, which just makes it unambiguously worse.

I loathe that in C# every parameter name is part of the public API because of this.

In C# I make structs to hold the arguments if it's not a situation where positional is appropriate. I find that's better anyway, since if there're enough parameters that naming them helps, it also helps to be able to build them all up over time, rather than needing them all in the function call.

Hmm, variable names with a common suffix? Sounds like it's missing structure. How about this?

predict_mixture( (HYDROGEN, 0.00003), (OXYGEN, 0.00025) )

Conveniently that even seems to be the same tuple type that the function is returning...

(Could even take a slice of them, perhaps, depending on what the method is supposed to be doing, where using stucture like this is clearly better than fn (&[SubstanceEnum], &[Liter]).)

1 Like

Since this drifted onto named arguments:

The problem with (implicit) named arguments is that the calling site context of a value may be different than the use sure name. The poster example in Swift would probably be

func send(message: Message, from source: Actor, to target: Actor)

In this case, send(message:source:target:) would also make sense, but send(message:from:to:) (arguably) reads better. Actually, it should probably be send(:source:target:), as the fact that the first parameter is Message is in the type, and the Swift API recommendations say that you shouldn’t repeat the type as an argument name.


Though I agree that the barrier should be high to getting features into the language, I don’t agree that it isn’t already. For one, writing a good RFC is already a large hurdle to overcome. Many suggestions die a fast, quiet death because someone tossed it out here or elsewhere, but nobody was enthused enough to write up a proper RFC. Most of the recent big-ticket changes that have gone through RFC have been language (or libs) team proposals, as well; the ones that come to mind are the modules and async.

Both of these come from a position of a weakness in the language: modules because Rust never intended to be revolutionary on the module front – and the last version I heard about was quite minimal, and async because one of Rust’s goals for 2018 is to be great choice for web-connected workers, which requires good, powerful, and ergonomic asynchrony.

(If you disagree that the modules change is small, here is the four bullet point version:

  • Allow crate-local paths to start with crate::
  • Allow paths to start with an external crate linked in by the rustc command line invocation (used by cargo)
  • Include linked in crates in the prelude names implicitly glob imported into any namespaces
  • Allow file.rs to have submodules as if it were file/mod.rs (currently, it can’t have any)

That seems minimal, incremental, and like a simplification to me; the root module no longer is special and information in cargo doesn’t have to be repeated.)

It only feels like a lot because there is a lot of enthusiasm about Rust from the community. That enthusiasm also helps to filter the RFCs to the ones that have a decent chance of improving the language. Almost every libs RFC is immediately hit by “why not an external crate” (those that don’t because they’ve addressed it), and even many language ones as well.


I do agree that accepting RFCs faster than they can be implemented is a dangerous road. That’s why I threw my hat in the “Rust 2018 should be boring” ring. But a key part of Rust’s philosophy is that “stability without stagnation”, so new ideas will hopefully always be coming.

As a final side, it’s very easy to do “armchair” language design. I’m not free of guilt here. It’s much easier to try to get your favorite feature (for me, refutable/guard let) into an existing language than make your own (plus convincing others to use it), and Rust is by far the most accepting of any idea from an outsider to language design. While we’re still in that position of hip new language that is open to community suggestions, and especially while we’re the “best” with those qualifications (for some (conflicting) definitions of “best” by the crowd), we will attract suggestions.

It’s our job as an engaged community to encourage good, thought out discussion and design. I hope we can continue to do a good job at it.


(Apologies for the wall of text; TL;DR change is scary but we should always evaluate objectively and critically (in the good way))

4 Likes

Consider reviving the named arguments thread :wink:

Edit: I just browsed through this and whoaa that's a really long thread. The summary post in (roughly) the mid of it is a nice read.

If an RFC has many people disagreeing, either via comments or votes, that should at least trigger some more extensive discussion about why the language team still went ahead. At the moment it just feels like a dismissal of the people who are against things.

I've more the feeling, that quite a few people feel a bit too much entitled, that their opinion has to be considered.

Just because the language team does open discussions about language features, doesn't mean that every contribution of people without experience in language design is valuable.

IMHO the team still does a great job in discussions and responds on different viewpoints. But the expectations what the language team should do are way too high.

I think because the language team has opened up that much, the expectations are also rising. It seems that you can't be too much open.

2 Likes

That always happens of course. In such cases, I think the perfect response is to either post a short bullet list with reasons or (even better) to link to a previous discussion or comment that addresses the point.

(BTW IMO you should change "that their opinion has to be considered" to "that their opinion is what counts". I think the lang team is actually considering every opinion because I've the impression that they always read all the comments)

1 Like

(BTW IMO you should change “that their opinion has to be considered” to “that their opinion is what counts”. I think the lang team is actually considering every opinion because I’ve the impression that they always read all the comments)

Yes, bad wording on my side.

1 Like

So, in your opinion I'm not allowed to ciriticise the current process? Asking for improvements is entitlement?

The language team has always been about involving the community. If that has changed, they should let people know. And yes, after spending days or weeks discussing a topic, it would be nice to have some form of result that you can put in context of your own ideas.

It's quite disappointing how criticism is treated these days.

Given that the definition of heuristic is, "a commonsense rule (or set of rules) intended to increase the probability of solving some problem", I don't see how heuristic is a better word. In fact, I'd say it is a worse word for the concept being described.

Yes, you could. But that requires more work on your part. Also, personally, I don't think declaration of tuples with greater than 2 or 3 members should be permitted in function declaration parameter position which would take the "work-around" off the table.

Why? How does having n named parameters be part of the public API differ in complexity from having a public structure with n named elements be part of the public API of a method? The latter has more complications, not less. In order to never have functions/methods with many parameters, you suggest always creating a structure instead? That sounds really counter-productive and I can already hear the wailing and gnashing of teeth.

You are changing the discussion from how to best call a function that someone else wrote to how best to write/design the function. In most cases, I would agree with you here on design of methods/functions, but, not always. If you think it is always, then, why not disallow it (which, by the way, I never suggested)?

Again, my comment is about how best to call methods written by others that have lots of parameters - it has nothing to do with how best to design a method/function.

So, just to be clear, when I say that Rust should support/require named parameters in method calls when the method has over a certain threshold of parameters, I am talking about how best to consume such methods written by others. I don't seek to disallow methods with lots of parameters. I don't seek to encourage methods with a lot of parameters. If you have a real need to write a method with a lot of parameters, then, the correct calling convention should be named parameter style. That is what I am advocating.

If your answer to that is, "Well, you should NEVER be creating methods/functions with lots of parameters" (which I disagree with), then why allow methods/functions with lots of parameters to be created?

TL;DR: If Rust allows high-cardinality parameters on functions, then, it should require named parameter calling convention for such functions. If the argument is it should never be done (which I disagree with) then don't allow such methods to be defined.

I'll take this feedback with me to the next meeting.

It hasn't changed! There are just very busy people in the teams, so sometimes it takes time to give feedback. I think the Async trait RFC shows that the teams listen, and do change their proposals or often do ask for changes given community feedback.

I think this happens; In the recent try RFC, @scottmcm wrote a long review discussing the ups and downs and took the feedback into account.

I try to read as many comments as I can, but I'm just a shepherd :wink: Of course the teams don't all read literally every comment (because that would be everything they could do in that case), but they do as best they can.

2 Likes