Blocks.

Passing Blocks

最常见,最简单,最富争议,最具有 Ruby 风格方式的闭包是 blocks。

接下来我们介绍 Ruby 中一个非常有特色的 feature —— blocks,它的存在使得 whilefor 这种传统的循环控制语句在 Ruby 中的使用率大大降低。例如,以下程序将重复输出三次 hi:

1
2
x = 3
x.times { puts "hi" }

These blocks are almost closures. 也就是说,blocks 是一种形式的 function value,其包含:

  • code of function body.
  • environment: variables in scope where the block is defined.

each 是一个常见的与 blocks 交互的方法。对于 list 中的每个元素,each 方法执行一次传入的 block。执行下述程序后变量 y 将变为 10:这是因为对于数组中的每个元素,each 都将执行一次 y += 1 语句。

1
2
y = 7
[4, 6, 8].each { y += 1 }

另外,block 也能 take arguments:与函数一样,block is executed under the environment (where it is defined) extended with the argument bindings. 在 block 中定义形参的语法是 { |i| e }do |i| e end。其中 i 为输入 block 的参数,e 为被执行的代码主体。

当某个 block { |i| e } 被传入 each 方法时,list 中的每个元素将会作为参数逐个传入 block,与 i 绑定后在扩展后的环境中执行 e。以下是一个经典的统计数组中数字之和的程序,执行它将逐行输出 4,10,18。

1
2
3
4
5
sum = 0
[4, 6, 8].each { |x|
sum += x
puts sum
}

Blocks, surprisingly, are NOT objects.

We cannot pass blocks as “regular” arguments to a method. Rather, any method can be passed either 0 or 1 blocks, after any other “regular” arguments.

inject 方法与 ML 中的 fold 类似,其维护一个参数 accumulator 并逐个处理数组中的元素。我们可以看到,inject 本身需要传入 accumulator 的初始值,此即为所谓 regular argument;而作为非对象的 block 在所有 regular argument 之后被传入。

1
sum = [4,6,8].inject(0) { |acc, elt| acc + elt }

inject 方法而言,传入其中的 block 可以有两个参数 acc, elt。其中 accinject 中的 accumulator 绑定,elt 逐个与数组中的元素绑定。

当调用允许传入 blocks 的方法时,我们需要了解该方法允许向 blocks 中传入参数的个数。对 inject 方法来说,允许的参数个数为 2;对 each 方法来说,允许的参数个数为 1;但当我们不需要向 blocks 中传入参数时,在定义中忽略 | ... | 部分即可。

在 Ruby 中,很多容器 (collections) 都定义了大量的 block-taking methods;这使得它在某种程度上实现了一部分 functional programming 的功能。

我们可以模仿 ML 中的定义方式,将 blocks 视作某个匿名函数的值,实现 mapfilter (select as in Ruby) 等类高阶函数。any?, all? 等遍历方法结合 blocks 更是在很多应用环境下替代了循环控制语句。

Using Blocks

We can define our own block-taking methods. We just pass a block to any method, and method body calls the block using the keyword yield.

1
2
3
4
5
6
7
8
9
10
def foo x
if x
yield
else
yield
yield
end
end
foo (true) { puts "true" }
foo (false) { puts "false" }

The above code prints "true" and then prints "false" 2 times. We could also pass arguments to a block by simply putting arguments after the yield, e.g., yield 7 or yield(8, "str").

The fact that a method may expect block is implicit; it is just that its body might use yield.

当方法的主体部分中使用了 yield 但调用方法时未向其传入 blocks,程序将会报错;In situations where a method may or may not expect a block, often other regular arguments determine whether a block should be present. If not, use block_given? method.

1
2
3
4
5
6
7
def count i
if yield i
1
else
1 + (count(i+1) { |x| yield x })
end
end

以上的方法将以递增的参数调用传入的 block,并统计最终在第几次调用时返回 true。注意到这个奇怪的 block:{ |x| yield x },it passes the caller’s block as the callee’s block argument.

在第 n + 1 层递归中,yield 将调用这个 “pipe” block。而其主体部分 { |x| yield x } 中又将执行一次 yield;而在参数绑定之后,该 yield 调用的是第 n 层递归中的 block。

这就是 lexical scope 的威力:它保证了 pipe block { |x| yield x } 被执行时的环境是其被定义处的环境 (即第n 层递归中的环境),以这种方式实现了 block 由 caller-side 到 callee-side 的传递。

Blocks are not first-class values: we cannot store them in a field, pass them as a regular method argument, assign them to a variable, put them in an array, etc.

再次强调,blocks 并不是对象:因此 (在 Ruby 中) 其并非“一等公民”。这是它与 closure 最大的区别。但是 Ruby 提供了将 blocks 升级为一等公民的方式。

Proc 类是真正的 closure,其实例化的对象可以被储存,作为普通的参数传入函数……我们只需要调用 lambda 方法,就能将 blocks 升级为 Proc 类的对象。

Ruby’s design is an interesting contrast from ML and Racket, which just provide full closures as the natural choice. In Ruby, blocks are more convenient to use then Proc objects and suffice in most uses, but programmers still have Proc objects when needed. 这是一个很值得探讨的问题:

Is it better to distinguish blocks from closures and make the more common case easier with a less powerful construct, or is it better just to have one general fully powerful feature?