Optional closure syntax

This is more of a light musing than passionate appeal, so I won't be debating this vigorously, but I thought I'd offer it as a suggestion.

I learned closures and function literals through Scala, and compared to the Rust syntax, I appreciate the clarity of the Scala syntax provided by the separator between parameters and body.

val addFunc = (x: Int, y: Int) => x + y

An identical definition in Rust would be

let addFunc = |x: i32, y: i32| x + y

I like the use of => in Scala to separate the parameter declaration from the function body. I realize that this operator is already in use for match, so a non-starter there, but I think there is an opportunity to use an unused operator such as ~> in its place.

let addFunc = |x: i32, y: i32| ~> x + y

I suggest this because, even as someone who understands and recognizes the closure syntax, the current syntax looks like a run-on sentence; to me the | doesn't sufficiently differentiate the parameters from the body and it looks like a single expression. The use of an arrow operator, to me, indicates that | THESE THINGS | are doing ~> this() + stuff()

I also realize that ergonomics is an objective for Rust, but I think two characters is reasonable.

Finally, by making it optional, you avoid breaking changes, although I would be curious to see usage statistics down the road if implemented.

Anyway, just wanted to make a passing suggestion. I'm not a language expert or anything, so take this with a grain of salt.

Cheers

rust already uses an arrow for the return type of a closure.

|x: u8, y: u8| -> u8 { x + y }

4 Likes

Ah yea, I forgot the return type. I guess in that case it would be more

|x: i32, y:i32| -> i32 ~> x + y

Scala has the benefit of not using an arrow for return type

(x: Int, y: Int): Int => x + y

Please note that changes to (existing) syntax are a (relatively) frequently requested thing, and have a relatively high bar for actually being accepted.

It’s almost impressive how much of that section (click the link above :wink:) seems to apply here

These also include proposals to add "alternative" syntaxes, in addition to those that replace the existing syntax. Many of these proposals come from people who also write other languages. Arguments range from the ergonomic ("I don't want to type this") to the aesthetic ("I don't like how this looks").


I appreciate your thoughts and do myself sometimes feel like Rust’s choice is a bit weird, coming from functional languages myself, which all like some forms of arrows; but in the end, once used to it, I find this syntax works just as well, too.

I’m also regularly surprised by how much variety syntax for anonymous function really offers across programming languages… heck, even in mathematics, there’s very far from a single consistent syntax for defining a quick “helper function” [one of the most common I’ve seen would be to use the “↦” operator; but also many will just prefer to always give a function a name, anyway].

I wonder, just out of curiosity, is there a good writeup giving an overview of the different kinds of closure/lambda/anonymous-function syntaxes in programming languages out there? And does Rust’s |args| expr syntax come from the specific precedent of some (or multiple) other languages?


Do note that a closure is “a single expression”, though that’s probably not what you meant. Also, as far as I’m aware, the style enforced by rustfmt is anyways for any case of a multi-line closure to wrap the RHS in a block… which IMO generally suffices to make sure the closure is hard to miss syntactically.

In fact, thinking back to examples like Haskell[1] with the multi-line case in mind, I find perhaps that’s the biggest strength of the arrow-less syntax… at the end of a line, a right-arrow to “nothing” just looks a bit silly, e.g. compare

(0..100).for_each(|i| {
    let x = i * i;
    println!("{i} squared is {x}");
});

to something like this proposal’s

(0..100).for_each(|i| ~> {
    let x = i * i;
    println!("{i} squared is {x}");
});

I don’t think bringing even more optionality into closure syntax is necessarily a great idea. There’s already the optional type signatures for arguments; optional specification of the return type; the option to wrap the RHS in a block or not; and even some subtle interactions of these optionalities[2]


  1. E.g. when conventionally desugaring “do notation”, you might get code such as

    action1 >>= (\ x1 ->
      action2 >>= (\ x2 ->
        mk_action3 x1 x2 ))
    

    from desugaring

    do { x1 <- action1
       ; x2 <- action2
       ; mk_action3 x1 x2 }
    
    ↩︎
  2. if you do specify a return type, then the block becomes part of the closure syntax itself, which has subtle operator precedence consequences, e.g. |x| -> Ty { …block… }.some_method() vs |x| { …block… }.some_method() applying some_method outside of the closure to the whole closure, or inside of it only to the returned block ↩︎

3 Likes

That's fair, and the syntax is manageable, just not aesthetically pleasing. I guess with the consideration of block syntax as option on the RHS, I'd probably advocate for making that required rather than introducing another arrow on top of the return type arrow. But, I suspect changing a syntax from optional to required is less likely to happen than my original suggestion, so I will simply curse under my breath every time I read and write it :slight_smile:

I’m just realizing, … something you could totally do in your own code at least … if you simply always use the closure syntax with “explicit” return type, even leaving the return type explicitly inferred via _, that gives you some presence of an arrow; and also forces the presence of braces with it, too :grin:

Whichever you prefer from

let addFunc = |x, y| -> _ { x + y };

or

let addFunc = |x: i32, y: i32| -> _ { x + y };

or

let addFunc = |x: i32, y: i32| -> i32 { x + y };

Add to this the explicit () for closures “without” a return value, and that might be some nice explicitness, too :thinking:

(0..100).for_each(|i| -> () {
    let x = i * i;
    println!("{i} squared is {x}");
});
10 Likes

It's Ruby I believe.

3 Likes

Ruby is (IMO) substantially different: the |args| goes on the inside of the braces, not before them.

1 Like