Ruby中的Proc,有两种,一种是 Proc 一种是 Lambda,能够经过 lambda? 来检测是否为lambda。其实lambda就是proc的另一种形态:数组
> ->{} # 建立一个lambda => #<Proc:0x007fc3fb809e60@(irb):46 (lambda)> # 能够看到返回的是Proc对象
> ->{}.class => Proc # 确实是Proc对象
Proc 和 Lambda 和区别是,Proc至关于代码植入,而Lambda是函数调用,因此Proc能够翻译为代码块,而Lambda,就叫匿名函数好了。
lambda由于是匿名函数,因此会在参数不对时候抛出异常,而proc就没有这样的问题,其次在proc内return会致使整个调用当即返回,后面的代码再也不执行,而lambda则不会,因此永远不该该在proc中写return语句。ruby
Proc建立方法有两种:
socket
Proc.new{|x| x + 1 } proc{|x| x + 1 }
能够查查是否是真的proc、仍是lambda:
ide
> Proc.new{}.lambda? => false > proc{}.lambda? => false
注:在Ruby1.8中,Kernel#proc() 实际上是 Kernel#lambda()的别名,由于遭到大量Rubyists反对,因此在 Ruby1.9版本中,Kernel#proc()变成了 Proc.new()的别名了。
Lambda也有两种
函数
lambda{|x| x + 1 } -> x { x + 1 }
检查是否为 lambda:
测试
> lambda{}.lambda? => true > ->{}.lambda? => true
Proc和Lambda都有如下四中调用方式:
this
pc = Proc.new{|x| x + 1 } pc.call(x) pc[x] pc.(x) # Ruby1.9 增长 pc===x # Ruby1.9 增长
对于最后一种 === 三个等号,资料很是少,单个参数调用proc/lambda均没有问题,但当有多个参数时:
spa
> proc{|x,y| x+y } === 1,2 => SyntaxError: (irb):189: syntax error, unexpected ',', expecting end-of-input
发现,没法调用,语法解析出错了。
翻译
> proc{|x,y| x+y } === [1,2] => 3
调用成功!由于proc支持传递数组,并将数组自动展开,因此能够正常调用。
但当是lambda时
rest
> lambda{|x,y| x+y } === 1, 2 => SyntaxError: (irb):191: syntax error, unexpected ',', expecting end-of-input
> lambda{|x,y| x+y } === [1,2] => ArgumentError: wrong number of arguments (1 for 2)
根本没法调用,由于lambda须要检测参数个数,而且不会将数组展开,这里的简便写法不正确。
查看 rubinius源代码 :
alias_method :===, :call
能够看到,三个等号就是 call的别名,这个方法没什么特别的, 🐹尝试这样写:
> lambda{|x,y| x+y}.=== 1, 2 => 3
这里加了个点号,固然也能够写成:
> lambda{|x,y| x+y}.===(1, 2) => 3
这里的等号和括号直接不能有空格,要是写成 .=== (1,2) 就会出错。
因此,推荐使用一下三种方式调用 prod/lambda,仅这一个参数的时候才用 ===,三等号只是让你写DSL的时候看起来更清爽一点,尽可能少用。
pc.call(x,y) pc[x,y] pc.(x,y)
stackoverflow上有人问,为什么proc/lambda调用必定要注明 .call / .() / [] 这样,为什么不能省略,就像方法那样,直接调用?
好比 pc ,那是由于ruby调用函数、方法时,能够省略圆括号,(), 这样解析器就没法区分究竟是在传递proc,仍是在调用他!
Ruby1.9开始 lambda支持参数默认值
> ->(x, y=2){ x+y }.(1) => 3
Ruby的Object,都有一个send方法,能够经过他传递方法名称,实现动态调用方法,
好比:相加
> 1.send(:+, 2) => 3
取子字符串
> "hello".send :[], 0,1 => h
至关于 "hello"[0,1]
但除了send还有一个如出一辙的 __send__ 方法,为什么会有两个名字?由于最先是只有send的,但考虑到不少场景好比socket会使用send来发送数据包,用户也可能会自定义send方法,这样就没法调用发送原先的send方法,因此又出来一个__send__来,这个方法不该该被override的。
Ruby的map原本只接收proc,但有时候却能够这样写:%w(a b c).map(&:upcase)
> %w(a b c).map &:upcase => ["A", "B", "C"]
&符号表示后面传递的变量实际上是一个proc,因此等价成这样:
> %w(a b c).map &:upcase.to_proc => ["A", "B", "C"]
当ruby发现后面跟着的不是proc对象后,将会调用该对象的to_proc方法,实现将其转换成proc对象:
Proc.new{|obj| obj.send :upcase}
若是没用定义to_proc,那么调用失败。
之因此map后面能够传入一个Symbol对象,是由于Ruby实现了Symbol对象的to_proc方法,这种用法最先出如今Rails里,
在Ruby1.8.7里面原生实现了Symbol对象的to_proc方法。
查看Rubinius里的实现:
class Symbol def to_proc # Put sym in the outer enclosure so that this proc can be instance_eval'd. # If we used self in the block and the block is passed to instance_eval, then # self becomes the object instance_eval was called on. So to get around this, # we leave the symbol in sym and use it in the block. # sym = self Proc.new do |*args, &b| raise ArgumentError, "no receiver given" if args.empty? args.shift.__send__(sym, *args, &b) end end end
能够看到,为了不send方法被复写,rubinius里使用的是__send__方法。
来实现一个最简单的to_my_proc方法
class Symbol def to_my_proc Proc.new{|obj| obj.send self} end end
测试:
> %w(a b c).map(&:upcase.to_my_proc) => ["A", "B", "C"]
调用成功了,对比这个简单的to_my_proc和Rubinius实现的差异,第一,proc只接收一个参数,忽略了其他的参数,
而rubinius则将其他的参数也看成方法的调用参数,一并send给了obj调用,第二个差异是self在外面单独赋值一次,
避免调用instance_eval的时候被覆盖,第三个差异是,同时传递了可能传递的block参数。
但隐式调用to_proc,这种方式,是没办法传递更多的参数的,好比要实现如下字符串解析成hash
'a:b;c:d' 转成 hash: {a=>b, c=>d}
基本写法:
> 'a:b;c:d'.split(';').map{|s| s.split ':' }.to_h => {"a"=>"b", "c"=>"d"}
使用to_proc简写:
第一步,先split成将要转成hash的数组
> 'a:b;c:d'.split(';').map &:split => [["a:b"], ["c:d"]]
split默认的参数是空格,因此转换失败。
添加参数试试:
> 'a:b;c:d'.split(';').map &:split(':') => SyntaxError: (irb):187: syntax error, unexpected '(', expecting end-of-input => 'a:b;c:d'.split(';').map &:split(':') ^
语法错误❌,这样写不行,由于:split(':')并非合法的Symbol对象。
其实,经过to_proc的源代码也能看出来,默认的to_proc方法不接受更多的参数。(由于定义 to_proc 后面根本没有根参数,其实跟了也没法传递,由于是隐式调用)
因此,改造本身的to_my_proc方法,让其接收更多的参数,而后显式调用:
class Symbol def to_my_proc(*args) Proc.new{|obj| obj.send self, *args } end end
> 'a:b;c:d'.split(';').map(&:split.to_my_proc(':')).to_h => {"a"=>"b", "c"=>"d"}
调用成功。
若是把方法名称定义为call而不是to_my_proc,则能够经过.()来调用,测试:
class TestCall def call(*args) puts "called: #{args}" end end
> TestCall.new.call "hello", "world" => call method called: ["hello", "world"] > TestCall.new.("hello", "world") => call method called: ["hello", "world"]
一样调用成功,证实ruby内部实现了.() 来表示.call 的别名,可是暂时没有找到实现的源代码。
因此把to_my_proc写成call:
class Symbol def call(*args) Proc.new{|obj| obj.send self, *args } end end
> 'a:b;c:d'.split(';').map(&:split.(':')).to_h => {"a"=>"b", "c"=>"d"}
调用成功,即便将该方法定义成 to_proc也不能隐式调用,由于后面的不是合法的Symbol对象,语法报错,因此虽然是支持了参数,但却必须显式调用该方法,返回一个proc对象供map调用。 这个to_my_proc/call写的很是简单,仅仅是传递了最基本的参数而已。stackoverflow上有写的更完善的代码:
class Symbol def call(*args, &block) ->(caller, *rest) { caller.send(self, *rest, *args, &block) } end end
这里用的是lambda,换成proc同样。
这样的方法在考虑到有多个参数传递到proc的时候,好比 each_with_index{|e, i| } 这样的状况,还有好比
[1,2,3].reduce(0){|r, e| r += e } ,这时候调用的proc会传递多个参数(放到*rest里),但可能结果不是想要的!好比实际调用可能会解析成:->(e, i) { e.send :方法, i, 其余参数 },至关于将剩余的参数也一并传递给了第一个参数:caller的方法调用了,因此在block内有多个参数时候,不建议用简写!
既然任何类只要实现了to_proc方法就能在前面加&符号,直接转换为proc调用,那么若是定义了数组的to_proc,一样能够,
因此,stackoverflow上有人想出了这样的代码:
class Array def to_proc Proc.new{|obj| obj.send *self } end end
这样就能够直接传递数组给map调用了,这里的*self就表明数组展开,测试代码:
> 'a:b;c:d'.split(';').map(&[:split, ':']).to_h => {"a"=>"b", "c"=>"d"}
估计用的人多了,Ruby可能会考虑内置这个方法,这个写法比Symbol的to_proc要更合适,尤为在须要传递参数给方法时,这种写法比起 &:方法.(参数) 看起来更直观。
在看inject/reduce方法,这个方法能够有如下一样有效的写法:
[1,2,3].reduce(0, &:+) [1,2,3].reduce(&:+) [1,2,3].reduce(:+) [1,2,3].reduce('+')
第一种写法没有问题,第二个参数做为proc传递,调用Symbol的to_proc方法将其转换成 result.send :+, item 方法调用,
第二个其实也没有太大疑问,由于缺乏初始值,实际内部循环只执行2次,第一次,1,不执行,第二次,用1做为初始值,执行带入block {|result, item| result + item } 获得最终值,
第三种和第四种,在map里若是直接写,会提示出错,而这里就能正常,缘由是reduce其实能够接收两个参数(而map是不接收参数的),当没有传递block时,会尝试将参数转换成symbol,而后调用symbol的to_proc方法将其转换成proc调用,因此等价成里第二种。
Rubinius实现代码:
def inject(initial=undefined, sym=undefined) if !block_given? or !undefined.equal?(sym) # 在没用block、或者有两个参数时! if undefined.equal?(sym) # 在只有一个参数的状况下,这一个参数必须为能够转换成方法调用,好比 :+。 sym = initial initial = undefined end # Do the sym version sym = sym.to_sym # 指定了to_sym方法的对象,均可以传递,因此能够传递 :+ 或者 '+' each do o = Rubinius.single_block_arg if undefined.equal? initial # 若是没初始值,将初始值置为第一个参数值,不执行sym转换成的proc代码 initial = o else # 不然正常执行sym转换的proc代码 initial = initial.__send__(sym, o) end end # Block version else # 当有block参数,而且只有一个参数调用的状况 each do o = Rubinius.single_block_arg if undefined.equal? initial # 没用传递初始值时候,将第一个值为初始值,不执行proc体代码 initial = o else # 将初始值带入proc执行。 initial = yield(initial, o) end end end undefined.equal?(initial) ? nil : initial end
由于上面我本身定义的Symbol的call方法并没用考虑 block后多个参数的状况,即inject/reduce的代码块:
[1,2,3].inject{|r, e| r + e },这种状况,因此若是尝试调用call会失败,稍微改造如下,让Symbol的call接收更多的参数:
class Symbol def call(*args) Proc.new{|first, *others| first.send self, *others, *args} end end
至于为何能够出现两个带*的参数,由于这不是函数定义,是函数调用,全部的带*参数会依次展开传入send方法调用,因此不会出现歧义。
调用:
> [1,2,3].inject &:+.() => 6
这里必定要加&符号,代表带入时proc调用,而非单个参数,由于若是是单个参数必定要能够转换为Symbol对象,而后再经过Symbol的to_proc实现调用,而 :+.() 显然不是合法的Symbol对象,而是对象调用call方法的简写,上面有说明。
一样,Array的to_proc方法也须要改造
class Array def to_proc sym = self.shift Proc.new{|first, *others| first.send sym, *others, *self} end end
> [1,2,3].inject &[:+] => 6
实际这样没什么用。
Ruby 2.3版本中, 新增了 Hash 的 to_proc方法:
> h = { foo:1, bar: 2, baz: 3} > p = h.to_proc > p.call :foo # 至关于 h[:foo] => 1
单纯这样没有什么用,可是能够用在map里,获取全部的值:
> [:foo, :bar].map { |key| h[key] } # h访问的是上面定义的h => [1, 2]
这样就能够简写为:
> [:foo, :bar].map &h => [1, 2]
能够本身实现一个简单的 Hash to_proc
Class Hash def to_proc Proc.new{ |key| self[key] } end end
暂时写到这里。