Ary Borenzweig 15 Sep 2013

to_proc

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.