跳至内容
GitHub 仓库 论坛 RSS-新闻源

to_proc

Ary Borenzweig

Ruby to_proc

Ruby 代码块非常强大。你可以很容易地将一组数字转换为字符串

[1, 2, 3].map { |n| n.to_s } #=> ["1", "2", "3"]

当然,你也可以使用这个快捷方式来实现

[1, 2, 3].map &:to_s #=> ["1", "2", "3"]

& 运算符将一个对象转换为一个 Proc,适合作为代码块传递。你可以通过实现一个 to_proc 方法,让任何类响应这个运算符。Symbol 有一个 to_proc 方法。

这一切都很不错,但如果你想向方法传递一个参数怎么办?例如

[10, 20, 30].map { |n| n.modulo(3) } #=> [1, 2, 0]

我们能写成 &:modulo(3) 来使其工作吗?事实证明 你不能,至少 不容易

不仅如此,由于 Ruby 需要将 Symbol 转换为 Proc,所以与使用普通代码块相比,会有一些性能损失。

最后,Ruby 实现的 Symbol#to_proc 有一个 Proc 缓存,所以它们不会在每次使用相同符号时都创建,但仍然比普通代码块稍微慢一些。

Crystal to_proc?

起初,我们考虑让 Crystal 有相同的语法来实现这一点,但有点笨拙:如果你写 &:to_s,因为 & 的参数是一个 Symbol,我们可以重写源代码来接收一个代码块

# This:
[1, 2, 3].map &:to_s

# is rewritten to this:
[1, 2, 3].map { |x| x.to_s }

对于其他参数,我们会做一些不同的事情(例如将函数类型转换为代码块)。

幸运的是,waj 提出了一个更好的建议:如果我们像 &.to_s 这样写呢?

[1, 2, 3].map &.to_s

现在,这是一个新的语法,不同于 Ruby。如果你在 Ruby 中这样做...

irb(main):001:0> [1, 2, 3].map &.to_s
SyntaxError: (irb):1: syntax error, unexpected '.'
[1, 2, 3].map &.to_s
               ^

这意味着在 & 后面加一个点在 Ruby 中没有意义,这也意味着这个语法可以用来赋予它新的意义。因此,在 Crystal 中,我们选择使用这种语法。

通过这个小小的改动,我们可以非常容易地向方法传递参数

[10, 20, 30].map &.modulo(3) #=> [1, 2, 0] ... but only in Crystal ;-)

不仅如此,你还可以写成这样

[1, 20, 300].map &.to_s.size #=> 1, 2, 3

或者这样

[[1, -2], [-3, -4]].map(&.map(&.abs)) #=> [[1, 2], [3, 4]]

当然还有这样

[1, 2, 3, 4].map &.**(2) #=> [1, 4, 9, 16]

最好的地方在于这只是一个语法重写,没有任何性能损失。