深入理解Ruby中的block概念
Ruby里的block一般翻译成代码块,block刚开始看上去有点奇怪,因为很多语言里面没有这样的东西。事实上它还不错。
First-classfunctionandHigher-orderfunction
First-classfunction和Higher-orderfunction是函数式编程语言里面的概念,听起来好像很高端的样子,其实很很简单的。
First-classfunctions是指在某些语言里,函数是一等公民,可以把函数当做参数传递,
可以返回一个函数,可以把函数赋值个一个变量等等,反正就是正常值能做的事函数都能做。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,所以第七行就可以调用这个函数了。
higher-orderfunction一般翻译成高阶函数,是指接受函数做参数或者返回函数的函数。
举个非常常用的例子(用JavaScript):
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作为参数。
其实matz早在一本书里《松本行弘的程序世界》里说了:
最终来看,块到底是什么? ... 块也可以看作只是高阶函数的一种特殊形式的语法。 ... 高阶函数和块的本质一样 ...
在Ruby里,函数不是一等公民,没有first-classfunctions。但是在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的基本上都是错的,
some和every这样利用短路求值的特点确实可以hack一下,但是明显不自然而且大大增加了别人理解代码的难度。
从这一点来看block确实还不错的。
只有一个函数参数的高阶函数
Ruby里一个方法只能接受一个block作为参数,大概就是类似于只有一个函数参数的高阶
函数。看起来好像是受到限制了。其实那本《松本行弘的程序世界》对此也有点解释。
大概是说了一个调查,在倾向于使用高阶函数的OCaml的标准库中,94%
的高阶函数只有一个函数参数。所以说这点限制不是什么问题。就我自己的体验来说,在JavaScript里,还从没用到需要两个函数参数的高阶函数。
未说明的
嗯,这篇文章看起来有点太长了,所以我不打算写下去了。其实还有一些重要的地方没说。比如
Block其实可以作为闭包用的。Ruby里用def定义方法时有点悲剧的,因为它不是闭包,接触
不到它外面的变量。
name="mike" defgreet puts"hello,#{name}" end hello#=>in`greet':undefinedlocalvariableormethod`name'formain: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"
同理还有class和module关键字都会创建新的作用域而在里面接触不到外面的变量,
也可以用block解决。
还有那个proc和lambda的区别。其实我一直不理解为什么会有人不用lambda
而跑去用proc,明显proc的return行为太不符合常识了。但是到头来却发现
block的行为跟proc创建的对象的行为是一样的,比如
defhello (1..10).each{|e|returne} return"hello" end hello#=>1
这感觉真是有点悲催。
结语
说了这么多,就是因为在Ruby里面函数不是一等公民,又想获得函数式编程的便利。