**Note:** Like before, I wanted to see how hard this was to implement. Nothing i…s decided yet.
Alternative to #9216
This PR lets you specify a block argument with references to block arguments. `&1` is the first block argument, `&2` is the second, and so on.
For example:
```crystal
[1, 2, 3].each &puts(&1)
```
The above is the same as:
```crystal
[1, 2, 3].each { |x| puts(x) }
```
You can use **any** expression after `&`. For example:
```crystal
[1, 2, 3].map &(&1 * 2) # => [2, 4, 6]
[1, 2, 3].map &{&1, &1 * 10} # => [{1, 10}, {2, 20}, {3, 30}]
[1, 2, 3].map &[&1] # => [[1], [2], [3]]
["foo.cr", "bar.cr"].map &File.exists?(&1)
["World", "Computer"].each &puts("OK #{&1}")
# Output:
# OK World
# OK Computer
record Point, x : Int32, y : Int32
[{1, 2}, {3, 4}].map &Point.new(&1, &2) # => [Point(@x=1, @y=2), Point(@x=3, @y=4)]
```
Since you can use **any** expression, this is valid too:
```crystal
# Not advised... if we really wanted we could disallow this
[1, 2, 3].each &begin
puts &1
end
```
## Do I like it?
I think I like it better than #9216 because:
- `&1`, `&2`, etc. are clearly surrounded by `&...`
- unless you use `begin`, or if we disallow it, the things you can put inside are pretty limited, so the block will end up being short and understandable
- no need to write `do/end` nor `{ ... }` so it's actually shorter than the alternative:
```crystal
["foo.cr", "bar.cr"].map &File.exists?(&1) # shorter, simpler
["foo.cr", "bar.cr"].map { File.exists?(&1) }
```
This is exactly the same in Elixir... except that in Elixir you can use `&(...)` in general to create functions, but in this PR you can only use it in a block argument.
## What happens with regular block arguments?
They still work! For example:
```crystal
proc = ->(x : Int32) { puts x }
[1, 2, 3].each &proc
```
The rule is that if there's `&1`, `&2`, etc. inside the block argument, then it gets expanded to a block. Otherwise it's just forwarded as a block argument. Because the type of the block argument needs to be a `Proc`, you will, in most if not all cases, get a compiler error if you try to misuse it. For example:
```crystal
p [1, 2, 3].map &puts("hello")
^---
Error: expected a function type, not Nil
```
And we can improve the error above saying "If using the short block syntax, be sure to use one of &1, &2, etc." (this PR doesn't do that yet, but it's super easy to implement).
Another alternative is to restrict the block argument to:
- a variable
- a proc literal (like `&->File.exists?(String)`)
- anything else, but it must include `&1`, `&2`, etc, otherwise it's an error
That alternative is probably better because we can give better error messages.
## Implementation details
For #9216 the first idea that came to my mind for implementing it is:
- before analyzing a call's block, check if it has `&1`, `&2`, etc
- if so, add missing block arguments, etc.
This means that the compiler has to do this for **every block** out there. I'm sure there are more optimal ways to implement this: for example we could detect at parse time which blocks need expansions. But this requires a bit more tracking.
In this PR we need to do the same, but only for block arguments (`&...)`, not for every block. And usually block arguments are just a forwarded variable, or a more complex expression which, with this PR, it's very likely to include these `&1` thingies. So the implementation is much more performant.
## Final thoughts
I think this solves the missing use case of short block syntaxes where one would like to put the first argument in a position other than the caller. And it also generalizes it for any number of arguments. All of this being kept surrounded by `&` so the code becomes easier to understand.
/cc @waj