Idea: allow `||` instead of `|_|` in closures that ignore all parameters

|_| comes up in a lot of uses of map, map_err, unwrap_or_else, and other methods that take closures. It’s annoying to type, and unsightly—to me at least!

I wanted to get people’s thoughts on allowing writing || expr in these cases when all parameters are being ignored. I think it fits in with Rust’s embracing of local inference to make things easier to type and read.

Is this something worth expanding into an RFC?

A couple examples from the Rust codebase:

// src/librustc_mir/borrow_check/nll/region_infer/values.rs:230
      self.add_internal(r, i, |_| cause.clone())

// src/librustc_metadata/locator.rs:873
            let buf = fs::read(filename).map_err(|_|
                format!("failed to read rmeta metadata: '{}'", filename.display()))?;

// src/librustc_typeck/check/closure.rs:99
        let substs = base_substs.extend_to(
            self.tcx,
            expr_def_id,
            |_, _| span_bug!(expr.span, "closure has region param"),
            |_, _| {
                self.infcx
                    .next_ty_var(TypeVariableOrigin::ClosureSynthetic(expr.span))
            },
        );

|| already defines a closure that doesn’t take any parameters. Changing that would break existing code.

10 Likes

I could get behind |..| to ignore all (0 or many) arguments.

17 Likes

The use of _ is intentional. Rust is strongly-typed. A closure that takes no arguments has a different type from that of a closure that ignores its argument. (Even in languages where this isn’t enforced by the compiler, many programmers prefer to spell out unused arguments for the sake of clarity. A colleague of mine had a pretty heated argument about this with a JavaScript-writing co-worker.)

This looks like yet another of those proposals that sacrifice type safety and introduce special cases and syntactic non-uniformity for shaving off one character. Please, just type out that _ instead.

17 Likes

Indeed... such an extension brings no new power to the language.

5 Likes

I’d be more in favor of something “explicit” like |..| for ignoring any number of arguments. Or indeed, just , .. for ignoring any remaining arguments. That is, basically applying the slice pattern annotations to closure arguments.

The advantage of this, in my opinion, has nothing to do with saving characters; when you are doing a refactor and you add an argument to the function that only will matter sometimes, this saves you from having to track down all the places you need to add , _ to.

I’d be interested in more concrete arguments about the dangers of allowing this than “Rust is strongly-typed.” In JavaScript, there are famous absurdities like map(parseInt) which produces nonsense, but I think this rule would not be sufficient to reproduce that. And I find the way this thread is being used as synecdochal of some dangerous trend in Rust’s design both flatly wrong & troubling in what it signifies about the open-mindedness of this forum to new ideas.

13 Likes

It's not about open-mindedness though. The problem is not all new ideas; the problem is ideas that contradict some of Rust's most important goals, safety and correctness, in the name of marginal "ergonomics". (Incidentally, if there happen to be more such proposals than genuinely good ones is not a question of attitude, it's a matter of fact.)

That seems like an argument against this feature. When I refactor, I'd pretty much like to explicitly see all places where the entity under refactoring is used. With this "ignore every argument after the first N" pattern, I would risk ignoring one where it would have been in fact necessary to use it. So this wouldn't "save" me from having to go through each use site – it would prevent me from deciding at every use site whether or not I need the new parameter.

This is basically the same problem as with the _ pattern in matches. Usually when I match an enum, I prefer to spell out every variant, even the ones that I don't use, so that if a variant is added in a future, I get a compiler error, and I can decide whether I need to explicitly handle it or I need to ignore it. It's not certain that I can just ignore it, so it's better to decide on a case-by-case basis. And in case you are wondering, it is not hypothetical – this practice has already saved me quite some painful debugging during refactoring in the past.

4 Likes

Pretty sure writing a closure like |a, b, ..| (a, b) is explicitly ignoring every argument after the first N.

I'm in favour of this (.. in closure args) syntax and would support adding it to the language given some good examples of code that would be improved by it. It makes a lot of sense with the rest of the language. But I'm also probably never going to personally use it for the same reason @H2CO3 avoids _ in matches.

3 Likes

This is the close-mindedness I am finding so frustrating. You are presuming a particular subjective position (that a feature would "contradict some of Rust's most important goals ... in the name of marginal 'ergonomics'") has both moral and factual authority.

3 Likes

Yes, you’re right, I meant “explicitly” – I amended my reply accordingly. (It doesn’t matter much, though. Ignoring arguments in general without considering each use individually is still a dangerous act.)

That “subjective” position has been backed up by arguments and examples.

I think that having |..| {} isn’t very dangerous. Changing the signature of a function is a breaking change and a library has to bump its major version to do it. Exactly the same is true from enums. For that reason I think that this is no more dangerous than a catch-all match arm or a match arm with a tuple pattern that contains ... @H2CO3 says that he avoids catch-all match arms because he wants to get notified by the compiler if an enum was changed. Same is true for |..| {}: If you don’t use it, it’s as if it doesn’t exist.

Here’s why I think |..| {} is an interesting idea:

  • It’s explicit
  • It’s self-explanatory
  • It seems useful
  • If you decide to not use it, it’s like it doesn’t exist
8 Likes

How does versioning help avoid bugs arising out of unintentionally ignoring an argument which should have been used? I don't follow.

That's only true as long as you only ever use code written by yourself, which is not realistic. It doesn't help with the errors others make in their own code that you want to use.

1 Like

I had a relevant experience recently in which I added a field to an enum variant. This variant already had 7 fields, and was matched over in a lot of places in the code base. Most of those places didn’t care about the field I was adding or several similar fields, because they are only relevant in a minority of cases. Really, the only existing code that cares about the field I added was the pretty printing module.

I had to change several locations where the variant was destructured (_, _, _, _, _, foo, _) to add another '_. There were many other locations, I saw by grepping, where the fields weren’t matched at all, and I fortunately didn’t have to edit it because it was just being matched ...

I don’t understand why closure arguments are different from destructuring? Why is it okay to have Pattern(..) but a bridge too far to have |..|? If one has been a part of Rust since before 1.0, how can the other contradict some of Rust’s most important goal?

(The code I was editing was adding async to the AST.)

6 Likes

Versioning helps because if you're using the closure inside a function from a library and the library authors really respect semantic versioning, changes to the signature of that function can't happen. (And if you change the signature of a private function, you can just update the places where you use the function with the callback)

Looking at this from this perspective adding this feature would actually make the language more consistent with itself.

1 Like

I feel that the discussion is already at a point where an RFE could be written to add this for the |..| {} syntax. I oppose the || {} syntax to ignore checking the arguments for the same reasons already expressed above.

8 Likes

@ekuber I think you mean “RFC” (request for comments) not “RFE” (request for enhancement), right? Just saying… probably the same thing anyway :slight_smile:

1 Like

:+1:

I like the consistency of making function parameter patterns more like tuple patterns, and I think it's consistent with a possible future extension (like in slice patterns) to variadic templates or tuple splatting or something.

8 Likes

@kamalmarhubi @tcr (or anyone else…)

Would you be willing to write up an RFC for allowing |..| and / or |x, ..|?

If you need any help in writing, you can ask for it here or join us at #rust-lang @ irc.mozilla.org.

Further summarizing arguments for the proposition of allowing |..| beyond what @MajorBreakfast wrote :

  • It is still explicit since special syntax must be used to say “I don’t care about the rest”.
  • It is readable; The meaning should be instantly recognizable for anyone who has seen .. elsewhere in patterns.
  • It is more readable than the status quo; because the status quo forces the reader to see redundant noise to the semantic intent of the author.
  • It is more ergonomic; because it removes noise, and the user doesn’t need to track the number of arguments and needs to write less when it applies.
  • It is consistent with rest patterns in tuples, and structs
  • It works with variadics
  • it is opt-in (this is a minor point)

Arguments for the opposition (I am biased, so…):

  • the exact structure of the arguments is less clear syntactically
  • less information is given to type inference by not “pattern matching” on the exact number of arguments.
6 Likes