Numbered parameters as block parameters

Hello.

I would like to discuss with you a proposal to introduce numbering parameters for blocks.

fn main() {
    let tupslice = [('r', 'u'), ('s', 't')];
    
    tupslice.iter().for_each(|(k, v)| println!("{} - {}", k, v));
}
fn main() {
    let tupslice = [('r', 'u'), ('s', 't')];

    tupslice.iter().for_each(println!("{} - {}", @1, @2));
}

Accordingly @1 is the first parameter @2 is the second, etc.

Sometimes you need to work with the code in the block and definition all the parameters through | | it is not very convenient especially when some of them are not used

fn main() {
    let users = [
      ("Andrew", "Mins", "andrew.mins@mail.com"),
      ("Kali", "Dork", "kali.dork@mail.com"),
    ];
    
    let _mails: Vec<&str> = users.iter().map(|(_first_name, _last_name, email)| *email).collect();
    
    dbg!(&_mails);
}

It can be rewritten as

let _mails: Vec<&str> = users.iter().map(*@3).collect();

If it is necessary to explicitly specify the data type, we can use the yet unstable functionality like type ascription

map(*@3 : &str)

Friends it will be useful?

I do not see any relation to the Unsafe Code Guidelines -- if you selected that category by mistake, can you edit and move to a more appropriate category?

1 Like

This feature seem really disturbing regarding the current Rust syntax for something that seem pretty much an edge case.

Writing a few _ in the closure argument list does not seem a so huge problem to me.

2 Likes

I do think currying / partial application / pointfree programming needs to become more convenient in Rust (and it's far from an edge case), but:

  1. Not right now, as we have other ongoing things with higher priority
  2. I'm not sure @index is the right syntax; it bears some discussion, although it does seem quite flexible.
5 Likes

Swift uses $0, $1, etc.

It also automagically turns blocks into closures if the method called takes a closure as an argument. In Rusty terms it'd be something like:

iter().for_each { println!("{} - {}", $0, $1) }

It's neat, but it also transforms the code in a way that is not immediately obvious.

Fixed. thx

The biggest issue with "inferred closure" proposals so far is scoping. Let's just say with your example, I have

users.iter().map(redact(@3))

How do we (the reader first, but also the compiler) know to which scope the @3 is attaching to closure?

3 Likes

Kotlin and groovy do the block to closure transformation as well. So while it's not immediately obvious, it is a mechanism that appears in a couple of different languages. I think it's nifty, but I'm not sold on it for Rust.

The examples given seem to compare this introduced syntax, to a pattern matched syntax, I think a more direct comparison would be against using the tuple indexing expressions

fn main() {
    let tupslice = [('r', 'u'), ('s', 't')];
    tupslice.iter().for_each(|x| println!("{} - {}", x.0, x.1));

    let users = [
      ("Andrew", "Mins", "andrew.mins@mail.com"),
      ("Kali", "Dork", "kali.dork@mail.com"),
    ];
    
    let _mails: Vec<&str> = users.iter().map(|x| x.2).collect();
    
    dbg!(&_mails);
}

Comparing those to the implicit closure mechanism, I much prefer the explicit closures clarity...

4 Likes

It's one of the most confusing feature of both languages; I found it especially annoying in Swift and I would not welcome it in Rust at all.

Also note that @ is part of patterns, where it means "separate name binding from subpattern". The @1 syntax would get that backwards in arguments, making argument patterns inconsistent with other patterns.

2 Likes

So to clarify, I love the block closure transformation in kotlin combined with the implicit this receiver, mostly because you can make amazing DSLs with them https://kotlinlang.org/docs/reference/type-safe-builders.html , though the example DSL in there isn't the best example.

I'm not really a fan of the other implicit parameter stuff though. I do like the recommendation to just the regular explicit tuple indexing notation.

2 Likes

There aren't really multiple parameters though -- for_each only gives a single parameter of I::Item, which happens to be a tuple in this case. Your first example just destructures that into its two fields. If you really want to number this, I'd lean towards @ratmice's example with tuple indexing.

1 Like

DSLs aren't a good way to sell this for Rust, (as you noted) because we already have a really good way to make DSLs, macros. Declarative macros are usually good enough to get a nice DSL, but with procedural macros, you can basically get whatever syntax you want as long as Rust can tokenize it. Which is a pretty lax limitation.

I don't think any block -> closure transformation would be good for Rust (as many have said already), because there are ownership/lifetime concerns wrt captures that can make this doubly confusing for Rust (especially newbies). Lifetimes are hard enough when things are relatively explicit, adding in implicit closures would just make things harder.

2 Likes

Oh, indeed. I agree. I just wanted to clarify what I liked about the feature set in kotlin. I originally mentioned kotlin/groovy more for comparison's sake than anything. But also it wouldn't be very likely in Rust either, macros not withstanding, because the implicit this receiver (which I never see, nor want Rust getting) goes a long way for ergonomics. And I very much like my Rust explicit.

I do pine for a way to give some sort of typing/autocompletion hints for macro contents though. That's the one edge that a closure based DSL would have - RLS support for autocompletion. But again, I don't think that trade off would be worth it.

1 Like

The implicitness makes code harder to understand (no longer clear which code ends up in a closure) and in principle ambiguous in case of nested closures, so I think adding this would be detrimental to the language.

3 Likes

Scala also has this problem. Scala even has it worse, where the expression _ + _ is equivalent to (x, y) => x + y.

I've written enough Scala to think this is a superbly bad idea. Curly-brace languages' syntax is, in my experience[1], largely incompatible with point-free style, where you don't have association guards like Haskell's $ that are not mixed up with the function call syntax or the scoping syntax.

[1] This mostly comes from trying way too hard to staple Haskell-ey things to Scala with nsc plugins in my misspent youth.

5 Likes