理解 Ruby 里的 block

Ruby 里的 block通常翻译成代码块,block 刚开始看上去有点奇怪,由于不少语言里面没有这样的东西。事实上它还不错。html

First-class function and Higher-order function

First-class functionHigher-order function 是函数式编程语言里面的概念,听起来好像很高端的样子,其实很很简单的。node

First-class functions 是指在某些语言里,函数是一等公民,能够把函数当作参数传递,
能够返回一个函数,能够把函数赋值个一个变量等等,反正就是正常值能作的事函数都能作。JavaScript 就是这样的。举个例子(下面的全部例子里,当我提到
JavaScript 时,示例代码都用的 CoffeeScript):编程

greet = (name) ->
  return -> console.log "Hello, #{name}"

greetToMike = greet("Mike")
greetToMike() # => 输出 "Hello, Mike"
a = greetToMike
a() # => 输出 "Hello, Mike"

在上面的第四行里,greet("Mike") 返回了一个函数,因此第五行里才能够调用 greetToMike()输出"Hello, Mike"。第六行把一个函数赋值给了a,因此第七行就能够调用这个函数了。api

higher-order function 通常翻译成高阶函数,是指接受函数作参数或者返回函数的函数。
举个很是经常使用的例子(用 JavaScript):ruby

a = [ "a", "b", "c", "d" ]
a.map((x) -> x + '!') #=> ["a!", "b!", "c!", "d!"]

上面例子里 map 就接受了一个匿名函数做为参数。Array.prototype里的不少方法,好比reduce, filter,every, some 等等都是高阶函数,由于他们都接受函数做为参数。闭包

高阶函数很是强大,表达力很强,能够避免大量重复代码。总的来讲,它就是个好东西。编程语言

Block 的本质

先来看一组 Ruby 和 CoffeeScript 代码的对比。函数式编程

a = [ "a", "b", "c", "d" ]
a.map { |x| x + "!" } # => ["a!", "b!", "c!", "d!"]
a.reduce { |acc, x| acc + x} # => "abcd"
a = [ "a", "b", "c", "d" ]
a.map((x) -> x + '!') # => ["a!", "b!", "c!", "d!"]
a.reduce((acc, x) -> acc + x) # => "abcd"

这两组代码真的看起来超级像。我以为这也暴露了 Ruby 的 block 的本质:高阶函数的函数参数的变体函数

JavaScript 里面的map 函数接受一个函数做为参数,可是 Ruby 里的 map 却接受一个
block 做为参数。prototype

其实 matz 早在一本书里《松本行弘的程序世界》里说了:

最终来看,块究竟是什么?
...
块也能够看做只是高阶函数的一种特殊形式的语法。
...
高阶函数和块的本质同样
...

在 Ruby 里,函数不是一等公民,没有 first-class functions。可是在 Ruby
里怎样使用高阶函数呢?答案就是使用 block。能够直接用 block,也能够用 lambda
或者 proc 把 block 转换成 Proc 类的实例用。

我发如今 Ruby 里使用 block 时,几乎全部的状况下均可以用 JavaScript
的高阶函数替代。

Enumerable 模块里的全部方法都是典型的例子。事实上确实存在 JavaScript 版
的 Enumerable,好比 Prototype.js 就有个 Enumerable,用起来跟 Ruby版的几乎同样的。固然它是经过高阶函数实现的。

与高阶函数有何不一样

除了语法上看上去有点不一样外,有很是重要的两点。

控制流操做

在 block 里面能够用 break, next 等等这些在通常的循环里才有的控制流操做,这些
在高阶函数里是用不了的。好比你能够试试在 JavaScript 里用 forEach 而不用循环
实现个take_while 函数,真是至关别扭的。好比以前 cnode 上就有人发帖问:nodejs的forEach不支持break吗?,其实这个帖子下面回复用 return 的基本上都是错的,
someevery 这样利用 短路求值 的特色确实能够 hack 一下,可是明显不天然并且大大增长了别人理解代码的难度。

从这一点来看 block 确实还不错的。

只有一个函数参数的高阶函数

Ruby 里一个方法只能接受一个 block 做为参数,大概就是相似于只有一个函数参数的高阶
函数。看起来好像是受到限制了。其实那本《松本行弘的程序世界》对此也有点解释。
大概是说了一个调查,在倾向于使用高阶函数的 OCaml 的标准库中,94%
的高阶函数只有一个函数参数。因此说这点限制不是什么问题。就我本身的体验来讲,在 JavaScript 里,还从没用到须要两个函数参数的高阶函数。

未说明的

嗯,这篇文章看起来有点太长了,因此我不打算写下去了。其实还有一些重要的地方没说。好比
Block 其实能够做为闭包用的。Ruby 里用def定义方法时有点悲剧的,由于它不是闭包,接触
不到它外面的变量。

name = "mike"
def greet
  puts "hello, #{name}"
end
hello # => in `greet': undefined local variable or method `name' for main:Object (NameError)

可是用 block 就能够了

name = "mike"
define_method(:greet) do
  puts "hello, #{name}"
end
greet # => "hello, mike"

用 JavaScript 就根本不存在问题。

name = "mike"
greet = -> console.log "hello, #{name}"
greet() # => "hello, mike"

同理还有classmodule 关键字都会建立新的做用域而在里面接触不到外面的变量,
也能够用 block 解决。

还有那个 proclambda 的区别。其实我一直不理解为何会有人不用lambda
而跑去用 proc,明显 procreturn 行为太不符合常识了。可是到头来却发现
block 的行为跟 proc 建立的对象的行为是同样的,好比

def hello
  (1..10).each { |e| return e}
  return "hello"
end
hello # => 1

这感受真是有点悲催。

结语

说了这么多,就是由于在 Ruby 里面函数不是一等公民,又想得到函数式编程的便利。

因此若是你以为 Ruby 太复杂了,赶忙去学 Elixir,简单优雅!

相关文章
相关标签/搜索