Ruby to_proc
Ruby blocks are powerful. You can easily convert an array of numbers to strings:
[1, 2, 3].map { |n| n.to_s } #=> ["1", "2", "3"]
Of course, you can also do this with this shortcut:
[1, 2, 3].map &:to_s #=> ["1", "2", "3"]
The &
operator converts an object to a Proc suitable to be
passed as a block. You can make any class respond to this operator by implementing
a to_proc
method. Symbol has a to_proc
method.
This is all very nice but what if you want to pass an argument to the method. For example:
[10, 20, 30].map { |n| n.modulo(3) } #=> [1, 2, 0]
Can we write something like &:modulo(3)
to make it work? It turns out
you can’t, at least
not that easily.
Not only that, but since Ruby has to convert the Symbol to a Proc there’s a slight performance penalty over doing it with a normal block.
Finally, Ruby’s implementation of Symbol#to_proc has a cache of Procs so they are not created every time you use the same symbol, but still, it’s slightly slower than a normal block.
Crystal to_proc?
At first we thought about making Crystal have the same syntax for this, but a bit hacky: if you
do &:to_s
, because the argument to &
is a Symbol we can rewrite the source code to receive a block:
# This:
[1, 2, 3].map &:to_s
# is rewritten to this:
[1, 2, 3].map { |x| x.to_s }
For other arguments, we would do something different (for example convert a function type to a block).
Fortunately, waj came with a better proposal: what if we write it like
&.to_s
?
[1, 2, 3].map &.to_s
Now, this is a new syntax, different from Ruby. If you do this in Ruby…
irb(main):001:0> [1, 2, 3].map &.to_s SyntaxError: (irb):1: syntax error, unexpected '.' [1, 2, 3].map &.to_s ^
This means that placing a dot after the &
makes no sense in Ruby, which also means that this syntax
is available for giving it a new meaning. So in Crystal we chose to use this syntax instead.
With this little change, we can pass arguments to the method very easily:
[10, 20, 30].map &.modulo(3) #=> [1, 2, 0] ... but only in Crystal ;-)
Not only that, but you can also write this:
[1, 20, 300].map &.to_s.size #=> 1, 2, 3
Or this:
[[1, -2], [-3, -4]].map(&.map(&.abs)) #=> [[1, 2], [3, 4]]
And of course this:
[1, 2, 3, 4].map &.**(2) #=> [1, 4, 9, 16]
The best thing is that this is just a syntax rewrite without any performance penalty.