Apr 07 2008

深入浅出Ruby的blocks, Procs和 methods

Posted by admin

Tags:

original from: understanding-ruby-blocks-procs-and-methods
author: eliben AT gmail.com
译者:g.zhen.ning
译注:此文章已得到eliben的同意翻译为中文,如果各位认位此文章有用,可以转载,译文的地址给不给出没所谓,但希望你能把原文的链接给出。
PS:此文我没校对,所以错译可能挺多,欢迎捉虫并进行修改。

简介
Rub借鉴了动态函数式编程的特性,如:闭包(closures)、高阶函数(high-order functions)、第一类函数(first-class functions),借此给程序员提供了一整套强大的特性。这些特性通过Ruby的代码块(code blocks)来实现。Proc对象和方法(都是对象—概念非常相近但有丝许差别。其实我对这个概念也感到非常困惑,极其难理解block,proc和method的分别,也不知道在哪种情况下用哪个来处理的效果比较好。另外,我虽也使用过Lisp和拥有多年Perl开发经验,但我不肯定Ruby的概念和其它编程语言的类似术语有什么关系,如Lisp的functions和Perl的subroutines。我阅读了大量新闻组的贴子后发现很多人都有这疑问,并且相当一部分新手被此折磨着。
在这篇文章中,我将展开我理解这些Ruby问题之旅,其中我参考了一些Ruby的书藉、文档和网站(comp.lang.ruby),我真诚地希望这篇文章能帮助到你。

Procs
我从Ruby文档中窥看到Procs被定义为:Proc对象是一组绑定了本地变量的代码块,,一旦绑定了,这代码可以在不同的上下文仍能访问这些变量。
一个实用的例子:

def gen_times(factor)
return Proc.new {|n| n*factor }
end

times3 = gen_times(3)
times5 = gen_times(5)

times3.call(12) #=> 36
times5.call(5) #=> 25
times3.call(times5.call(4)) #=> 60

Procs在Ruby里担任函数的角色。更准确的命名是函数对象,因为所有东西在Ruby里都是对象。如对象俗称-functors。Functor被定义为对象去invoked或者called,如果它是普通函数,通常这伴有相同的句法,这就是Proc。
从例子和上面的定义,显然Ruby的Procs可以作为closures。在Wikipedia,closure被定义为提交到词汇上正文变量的函数(function that refers to free variables in its lexical context)。注意它和Ruby定义的绑定到一组本地变量的代码块有多接近。

再论Procs
Procs在Ruby是first-class objects,因为他们可以在运行期(runtime)创建,储存在数据结构里,作为能数传递到其它函数并且作为其它函数的值返回。实际上,gen_times例子证明所有的这些例子,除了“作为参数传递到其它函数”,这个问题可以如下表述:
def foo (a, b)
a.call(b)
end

putser = Proc.new {|x| puts x}
foo(putser, 34)

这是一个创建Proc的简化符号-内核(kernel)方法lambda(不久将变为methods,但现在假设这kernel方法是全局函数的同族,这能在代码的任何地方被调用)[we’ll come to methods shortly, but for now assume that a Kernel method is something akin to a global function, which can be called from anywhere in the code]。使用lambda,之前例子的Proc对象创建能被重写为:
putser = lambda {|x| puts x}

实际上,lambda和Proc.new有两个微小分别。1、参数的检查,Ruby文档关于lambda的描述:除了当调用时Proc对象检查传递参数的个数。这个例子论证这一点:
lamb = lambda {|x, y| puts x + y}
pnew = Proc.new {|x, y| puts x + y}

# works fine, printing 6
pnew.call(2, 4, 11)

# throws an ArgumentError
lamb.call(2, 4, 11)

2、返回值的不同。Proc.new的返回值从封装(enclosing)方法(就如在block里返回一样,下面有详细的对比:)
def try_ret_procnew
ret = Proc.new { return "Baaam" }
ret.call
"This is not reached"
end

# prints "Baaam"
puts try_ret_procnew

当从lambda返回时更循从惯例,返回它的调用者。
def try_ret_lambda
ret = lambda { return "Baaam" }
ret.call
"This is printed"
end

# prints "This is printed"
puts try_ret_lambda

因此,我建议使用lambda来代替Proc.new,除非后者的行为严格要求(unless the behavior of the latter is strictly required)。两个参数的少一点意外(addition to being way cooler a whopping two characters shorter, its behavior is less surprising).

Methods
简单输出,method也是块代码,然后,它不像Procs,方法没有绑定到一组本地变量。正因为这样,他们绑定到一些对象并且可以访问它的实例变量。
class Boogy
def initialize
@dix = 15
end

def arbo
puts "#{@dix} ha\n"
end
end

# initializes an instance of Boogy
b = Boogy.new

# prints "15 ha"
b.arbo

有用的习惯用法是在方法发送消息。(A useful idiom when thinking about methods is sending messages.)。给接收者-拥有一些方法定义的对象,我们可以向它发送消息-通过调用这方法,是否有能数是可选的。在上面的例子,调用arbo是类似通过不带参数发送消息“arbo”。Ruby支持更直接发送习惯用法消息,通过包含send method在类object(object是Ruby类的父类)。所以下面这行代码都是arbo方法的调用:
# method/message name is given as a string
b.send("arbo")

# method/message name is given as a symbol
b.send(:arbo)

注意这方法能定义在“顶层”(top-level)区域,没有在任何类中。例子如下:
def say (something)
puts something
end

say "Hello"

这看来是“free-standing”,但不是。当方法如此被定义时,Ruby暗地里把它们挤进到Ojbect类中。但真的不要紧。and for all practical purposes say can be seen as an independent method. Which is,顺便说一下,一些编程语言(像C和Perl)他们称这为”function”。下面的Proc是,在许多地方有类似之处。
say = lambda {|something| puts something}

say.call("Hello")

# same effect
say["Hello"]

[]结构是Proc的上下文的同义字(The [] construct is a synonym to call in the context of Proc)
Methods,然而,它比procs更通用,支持更多重要Ruby特性,这我将在解释完什么是Block之后来阐述。

Blocks
Blocks和Procs的关系如此接近以致于许多新手为弄清他们的真正区别而头痛不已。我现在尝试用一些比喻来理解它(希望我的比喻不是那么差吧)。我感觉Block是未诞生的Procs(unborn Procs)。Blocks是其幼虫,Procs是成型后的昆虫。Block不是依靠自身生存-它准备的的代码是为了当它真正存活时而设的,并且只有当它绑定并且转换成Proc时,它才存活:
# a naked block can't live in Ruby
# this is a compilation error !
{puts "hello"}

# now it's alive, having been converted
# to a Proc !
pr = lambda {puts "hello"}

pr.call

就这样,这就是谜团所在,然后呢?不,不全是这样。Ruby的设计者,Matz留意到当传递Procs到methods(传递到其它Procs)很不错并且允许高阶函数和所有奇异功能的东西(stuff),有一个普遍案例比其他案例要重要—-传递一个单独的代码块到一个方法,这方法能从中做出有用的东西,例如迭代(iteration)。并且作为一个富有才干的设计者,Matz认为值得花时间去使这个特别案例(special case)变得更简单,更高效。

Passing a block to a method
无需怀疑的哪些至少花费过数小时在Ruby的程序员已经写出下面这些Ruby荣耀的例子(或者非常类似的代码):
10.times do |i|
print "#{i} "
end

numbers = [1, 2, 5, 6, 9, 21]

numbers.each do |x|
puts “#{x} is ” + (x >= 3 ? “many” : “few”)
end

squares = numbers.map {|x| x * x}
(注意:do |x| … end和{ |x| …} )
这些代码就是IMHO,Ruby的整洁、便于阅读、神奇、部分原因就在于此。幕后的情形非常简单,或者绝对可以用这种简单的方式来说明。或许Ruby并没有以我所描述的方式来实现,因为还存在其它有效的优化措施,但是这样的比方显然足够接近事实真相。

只要method调用时附加了block,Ruby会自动把此block转换为一个没有明确命名的Proc对象。不过,此method可以通过yield语句来访问该Proc对象。看下面这个例子来说明这一切。
def do_twice
yield
yield
end

do_twice {puts "Hola"}
这方法do_twice定义并且附加block调用。尽管这方法没有明确请求block到参数列中,但yield能调用此block。这也可以必为更明确的方法,使用Proc参数:
def do_twice(what)
what.call
what.call
end

do_twice lambda {puts "Hola"}
这和先前的例子是等同的,但使用yield来调用blocks更简洁并且当只有block传递到block是最优化的,想要明确的话,使用Proc的方法吧。下面这例子就是用参数来传递的:
def do_twice(what1, what2, what3)
2.times do
what1.call
what2.call
what3.call
end
end

do_twice( lambda {print "Hola, "},
lambda {print "querido "},
lambda {print "amigo\n"})

这对那些讨厌通过block传递,而用Procs来代替的朋友来说是很重要的例子。这基本原理:block参数是固定的,它会遍历(look through)整个方法来查看是否有calls在这里调用,当Proc明确立刻在参数列里发现。虽然这只是个人品味取向,但明白这二者的方法是有必要的。

&符号的使用
&运算符号能用在一些晦涩案例中(esoteric cases)中明确Block和Proc之间的转换,因此我们值得花上一些时间来弄清楚它是如何运作的。
还记得我说过虽然帽底乾坤(under the hood)是附加的block会转换为Proc,但是当Proc在方法的内部时不知道如何理解?是这样的!如果&运算符预先放在方法的参数列表的最后一个参数里,block会转换为一个Proc对象,并且赋值给这个参数:
def contrived(a, &f)
# the block can be accessed through f
f.call(a)

# but yield also works !
yield(a)
end

# this works
contrived(25) {|x| puts x}

# this doesn't (ArgumentError), because &f
# isn't really an argument - it's only there
# to convert a block
contrived(25, lambda {|x| puts x})

另外一种(依我的拙见,这更有用)使用&运算符的是用在另一种转化。将Proc转换为Block。这很有用,因为大部分Ruby内置类,特别是迭代器(iterators),这些都期望接收block作为参数,并且有时传递Proc给他们更方便。下面这个例子取自于pragmatic programmers的Programming Ruby
print "(t)imes or (p)lus: "
times = gets
print "number: "
number = Integer(gets)
if times =~ /^t/
calc = lambda {|n| n*number }
else
calc = lambda {|n| n+number }
end
puts((1..10).collect(&calc).join(", "))

Collect方法期望获得一个block,但在这个例子中提供Proc也非常方便,因为Proc是从用户的构造(knowledge)里生成的。Calc前的&符号确定Proc对象calc变成代码块(code block)并且作为附加block传递到collect方法中。
&符号同样允许应用在非常普通的Ruby程序员习惯用法(common idiom)中:通过方法名字进行迭代。假设我想将数组里所有单词转换成全是大写的。我或许会这样做:
words = %w(Jane, aara, multiko)
upcase_words = words.map {|x| x.upcase}

p upcase_words

这挺不错,并且是可行的,只是我觉得这有点冗长。Upcase方法应该不需要用块和添加x参数来返回给map。(The upcase method itself should be given to map, without the need for a separate block and the apparently superfluous x argument)。幸运的是就像我们之前看到的,Ruby支持发送消息到对象的习惯用法,方法也能通过他们的名字被关联(methods can be referred to by their names),就如Ruby Symbols的实现方法一样。如下例:
p "Erik".send(:upcase)

这样的实现,相当易理解,发送 消息/方法 upcase到”Erik”对象。这个特性可以利用来以一种儒雅的方式实现{|x| x.upcase},并且我们使用&符号来实现(we’re going to use the ampersand for this!),就如我之前所说的,当&符号放置在一些Proc前被方法调用(method call),Proc将会转换为block,但如果我放置的&符号后的并不是Proc,而是其它对象又会如何呢?是这样处理的:遇到这种情况,Ruby的隐性类型转换机制不用使用,而会尝试采用to_proc方法来调用该对象生成Proc。我们能这样来实现Symbol的to_proc来达到我们想要的效果:(then, Ruby’s implicit type conversion rules kick in, and the to_proc method is called on the object to try and make a proc out of it. We can use this to implement to_proc for Symbol and achieve what we want)
class Symbol

# A generalized conversion of a method name
# to a proc that runs this method.
#
def to_proc
lambda {|x, *args| x.send(self, *args)}
end

end

# Viola !
words = %w(Jane, aara, multiko)
upcase_words = words.map(&:upcase)

结论
Ruby没有真正意义上的函数,换来的是两个丝许不同的概念—methods和Procs(就像我们之前讲述的,就像其它程序语言称之为函数对象,仿函数。)两者都是代码块—methods绑定到Objects,Procs绑定到全局范围的本地变量。他们的使用截然不同。
Methods是面向对象的基础,因为Ruby是纯OO语言(所有东西都是对象),methods是Ruby本性与生俱来的。Methods是Ruby对象的actions—它们接收messages(如果你偏爱于用message发送习惯用语)
有了Procs,我们便能使用功能强大的函数式语言范式(paradigms),把代码转换成Ruby允许实现高阶函数的第一类对象(first-class object)。他们非常类似Lisp的lambda形态。(我对Ruby的Proc构造器lambda的起源有点疑惑)
第一眼看下去你可能会被block的构造搞得糊涂,但细心看看,其实是相当易懂的。让我用个比喻来概括一下吧,block就是一个未诞生的Proc—它是Proc的中间状态,暂未绑定任何东西。我认为完整理解Ruby里的blocks,是把blocks作为一个拥有真正状态的Procs,这样的概念更完整。Blocks和procs的些微不同是我们之前考察的一个特别的案例:当它们作为作为方法的最后一个参数,通过使用yield来访问它们。

我猜block就是这样。我非常相信我这个分析清除了许多误解。我希望其他朋友会从中获益。如果你有不同的意见—我文章里有显着的错误,甚至极小的差错。我都欢迎你的意见。我乐意和你进行讨论和改正错误。

脚注:
[1]这似乎完美的,理论的解释了Ruby没有第一类函数。然而,在文章的示便中看到,
Ruby完全有能力实现大部分第一类函灵敏的需求。也就是说函数能在程序运行期创建,储存在数据结构,作为参数传递并且作为其它函数值返回。

[2]lambda有个别名-proc,但目前来说有不太赞成使用它(主要因为proc和Proc.new只是丝微的不同,这令人很困惑)
换句话说,用lambda比较好。

[3]这些是实例方法,而且Ruby还支持类方法和类变量,但这不是本文所要讨论的。

[4]更精确的来说,call和[]都被指向到类Proc的相同方法。是的,Proc对象本身是有方法的!

Filed under : technology |

2 Responses to “深入浅出Ruby的blocks, Procs和 methods”

  1. kava Says:

    RUBY 挺好玩,才发现

    不过果然是没校对呢 - -!!!

  2. admin Says:

    是的,自己看明白差不多,也没太多时间去校对了,以前在ruby-lang.org.cn时有专人校对,专人译,这样省事一点。

Leave a Reply