class Proc

Proc 对象是对一段代码块的封装,它可以存储在局部变量中,传递给方法或另一个 Proc,并且可以被调用。 Proc 是 Ruby 中一个基本概念,也是其函数式编程特性的核心。

square = Proc.new {|x| x**2 }

square.call(3)  #=> 9
# shorthands:
square.(3)      #=> 9
square[3]       #=> 9

Proc 对象是闭包,这意味着它们会记住并可以使用它们被创建时的整个上下文。

def gen_times(factor)
  Proc.new {|n| n*factor } # remembers the value of factor at the moment of creation
end

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

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

创建

有几种方法可以创建一个 Proc

Lambda 和非 Lambda 语义

Procs 有两种类型:lambda 和非 lambda(常规 procs)。区别在于

示例

# +return+ in non-lambda proc, +b+, exits +m2+.
# (The block +{ return }+ is given for +m1+ and embraced by +m2+.)
$a = []; def m1(&b) b.call; $a << :m1 end; def m2() m1 { return }; $a << :m2 end; m2; p $a
#=> []

# +break+ in non-lambda proc, +b+, exits +m1+.
# (The block +{ break }+ is given for +m1+ and embraced by +m2+.)
$a = []; def m1(&b) b.call; $a << :m1 end; def m2() m1 { break }; $a << :m2 end; m2; p $a
#=> [:m2]

# +next+ in non-lambda proc, +b+, exits the block.
# (The block +{ next }+ is given for +m1+ and embraced by +m2+.)
$a = []; def m1(&b) b.call; $a << :m1 end; def m2() m1 { next }; $a << :m2 end; m2; p $a
#=> [:m1, :m2]

# Using +proc+ method changes the behavior as follows because
# The block is given for +proc+ method and embraced by +m2+.
$a = []; def m1(&b) b.call; $a << :m1 end; def m2() m1(&proc { return }); $a << :m2 end; m2; p $a
#=> []
$a = []; def m1(&b) b.call; $a << :m1 end; def m2() m1(&proc { break }); $a << :m2 end; m2; p $a
# break from proc-closure (LocalJumpError)
$a = []; def m1(&b) b.call; $a << :m1 end; def m2() m1(&proc { next }); $a << :m2 end; m2; p $a
#=> [:m1, :m2]

# +return+, +break+ and +next+ in the stubby lambda exits the block.
# (+lambda+ method behaves same.)
# (The block is given for stubby lambda syntax and embraced by +m2+.)
$a = []; def m1(&b) b.call; $a << :m1 end; def m2() m1(&-> { return }); $a << :m2 end; m2; p $a
#=> [:m1, :m2]
$a = []; def m1(&b) b.call; $a << :m1 end; def m2() m1(&-> { break }); $a << :m2 end; m2; p $a
#=> [:m1, :m2]
$a = []; def m1(&b) b.call; $a << :m1 end; def m2() m1(&-> { next }); $a << :m2 end; m2; p $a
#=> [:m1, :m2]

p = proc {|x, y| "x=#{x}, y=#{y}" }
p.call(1, 2)      #=> "x=1, y=2"
p.call([1, 2])    #=> "x=1, y=2", array deconstructed
p.call(1, 2, 8)   #=> "x=1, y=2", extra argument discarded
p.call(1)         #=> "x=1, y=", nil substituted instead of error

l = lambda {|x, y| "x=#{x}, y=#{y}" }
l.call(1, 2)      #=> "x=1, y=2"
l.call([1, 2])    # ArgumentError: wrong number of arguments (given 1, expected 2)
l.call(1, 2, 8)   # ArgumentError: wrong number of arguments (given 3, expected 2)
l.call(1)         # ArgumentError: wrong number of arguments (given 1, expected 2)

def test_return
  -> { return 3 }.call      # just returns from lambda into method body
  proc { return 4 }.call    # returns from method
  return 5
end

test_return # => 4, return from proc

Lambdas 作为自给自足的函数非常有用,特别适用于作为高阶函数的参数,其行为与 Ruby 方法完全相同。

Procs 对于实现迭代器非常有用

def test
  [[1, 2], [3, 4], [5, 6]].map {|a, b| return a if a + b > 10 }
                            #  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
end

map 内部,代码块被视为常规(非 lambda)proc,这意味着内部数组将被解构为参数对,并且 return 将从 test 方法退出。使用更严格的 lambda 将无法实现这一点。

你可以通过使用 lambda? 实例方法来区分 lambda 和常规 proc。

Lambda 语义通常在 proc 的生命周期内保留,包括 &-解构为代码块

p = proc {|x, y| x }
l = lambda {|x, y| x }
[[1, 2], [3, 4]].map(&p) #=> [1, 3]
[[1, 2], [3, 4]].map(&l) # ArgumentError: wrong number of arguments (given 1, expected 2)

唯一的例外是动态方法定义:即使通过传递非 lambda proc 定义,方法仍然具有正常的参数检查语义。

class C
  define_method(:e, &proc {})
end
C.new.e(1,2)       #=> ArgumentError
C.new.method(:e).to_proc.lambda?   #=> true

此例外确保方法永远不会有不寻常的参数传递约定,并且可以轻松地拥有定义行为如常的方法的包装器。

class C
  def self.def2(name, &body)
    define_method(name, &body)
  end

  def2(:f) {}
end
C.new.f(1,2)       #=> ArgumentError

包装器 def2 将 _body_ 作为非 lambda proc 接收,但定义了一个具有正常语义的方法。

其他对象转换为 procs

任何实现了 to_proc 方法的对象都可以通过 & 运算符转换为 proc,因此可以被迭代器使用。

class Greeter
  def initialize(greeting)
    @greeting = greeting
  end

  def to_proc
    proc {|name| "#{@greeting}, #{name}!" }
  end
end

hi = Greeter.new("Hi")
hey = Greeter.new("Hey")
["Bob", "Jane"].map(&hi)    #=> ["Hi, Bob!", "Hi, Jane!"]
["Bob", "Jane"].map(&hey)   #=> ["Hey, Bob!", "Hey, Jane!"]

在 Ruby 核心类中,此方法由 SymbolMethodHash 实现。

:to_s.to_proc.call(1)           #=> "1"
[1, 2].map(&:to_s)              #=> ["1", "2"]

method(:puts).to_proc.call(1)   # prints 1
[1, 2].each(&method(:puts))     # prints 1, 2

{test: 1}.to_proc.call(:test)       #=> 1
%i[test many keys].map(&{test: 1})  #=> [1, nil, nil]

孤立的 Proc

代码块中的 returnbreak 会退出方法。如果从代码块生成 Proc 对象,并且 Proc 对象一直存在到方法返回,则 returnbreak 无法工作。在这种情况下,returnbreak 会引发 LocalJumpError。在这种情况下,Proc 对象被称为孤立的 Proc 对象。

请注意,returnbreak 要退出的方法是不同的。存在 break 为孤立,但 return 不孤立的情况。

def m1(&b) b.call end; def m2(); m1 { return } end; m2 # ok
def m1(&b) b.call end; def m2(); m1 { break } end; m2 # ok

def m1(&b) b end; def m2(); m1 { return }.call end; m2 # ok
def m1(&b) b end; def m2(); m1 { break }.call end; m2 # LocalJumpError

def m1(&b) b end; def m2(); m1 { return } end; m2.call # LocalJumpError
def m1(&b) b end; def m2(); m1 { break } end; m2.call # LocalJumpError

由于 returnbreak 在 lambdas 中会退出代码块本身,因此 lambdas 不可能是孤立的。

匿名块参数

为了简化编写短代码块,Ruby 提供了两种不同类型的匿名参数:it(单个参数)和编号参数:_1_2 等。

# Explicit parameter:
%w[test me please].each { |str| puts str.upcase } # prints TEST, ME, PLEASE
(1..5).map { |i| i**2 } # => [1, 4, 9, 16, 25]

# it:
%w[test me please].each { puts it.upcase } # prints TEST, ME, PLEASE
(1..5).map { it**2 } # => [1, 4, 9, 16, 25]

# Numbered parameter:
%w[test me please].each { puts _1.upcase } # prints TEST, ME, PLEASE
(1..5).map { _1**2 } # => [1, 4, 9, 16, 25]

it

当没有定义显式参数时,it 是一个在代码块内部可用的名称,如上所示。

%w[test me please].each { puts it.upcase } # prints TEST, ME, PLEASE
(1..5).map { it**2 } # => [1, 4, 9, 16, 25]

it 是一个“软关键字”:它不是保留名称,可以用作方法和局部变量的名称

it = 5 # no warnings
def it(&block) # RSpec-like API, no warnings
   # ...
end

即使在将其用作隐式参数的代码块中,也可以将 it 用作局部变量(尽管这种风格显然会令人困惑)

[1, 2, 3].each {
  # takes a value of implicit parameter "it" and uses it to
  # define a local variable with the same name
  it = it**2
  p it
}

在定义了显式参数的代码块中使用 it 会引发异常

[1, 2, 3].each { |x| p it }
# syntax error found (SyntaxError)
# [1, 2, 3].each { |x| p it }
#                        ^~ `it` is not allowed when an ordinary parameter is defined

但是,如果可以使用局部名称(变量或方法),则将使用该名称

it = 5
[1, 2, 3].each { |x| p it }
# Prints 5, 5, 5

可以使用 it 的代码块可以嵌套

%w[test me].each { it.each_char { p it } }
# Prints "t", "e", "s", "t", "m", "e"

使用 it 的代码块被认为有一个参数

p = proc { it**2 }
l = lambda { it**2 }
p.parameters     # => [[:opt, nil]]
p.arity          # => 1
l.parameters     # => [[:req]]
l.arity          # => 1

编号参数

编号参数是隐式命名块参数的另一种方法。与 it 不同,编号参数允许在一个代码块中引用多个参数。

%w[test me please].each { puts _1.upcase } # prints TEST, ME, PLEASE
{a: 100, b: 200}.map { "#{_1} = #{_2}" } # => "a = 100", "b = 200"

支持从 _1_9 的参数名称

[10, 20, 30].zip([40, 50, 60], [70, 80, 90]).map { _1 + _2 + _3 }
# => [120, 150, 180]

但是,建议明智地使用它们,可能将自己限制为 _1_2,以及单行代码块。

编号参数不能与显式命名的参数一起使用

[10, 20, 30].map { |x| _1**2 }
# SyntaxError (ordinary parameter is defined)

编号参数也不能与 it 混合使用

[10, 20, 30].map { _1 + it }
# SyntaxError: `it` is not allowed when a numbered parameter is already used

为避免冲突,将局部变量或方法参数命名为 _1_2 等会导致错误。

  _1 = 'test'
# ^~ _1 is reserved for numbered parameters (SyntaxError)

使用隐式编号参数会影响代码块的 arity

p = proc { _1 + _2 }
l = lambda { _1 + _2 }
p.parameters     # => [[:opt, :_1], [:opt, :_2]]
p.arity          # => 2
l.parameters     # => [[:req, :_1], [:req, :_2]]
l.arity          # => 2

带有编号参数的代码块不能嵌套

%w[test me].each { _1.each_char { p _1 } }
# numbered parameter is already used in outer block (SyntaxError)
# %w[test me].each { _1.each_char { p _1 } }
#                    ^~