class Proc

Ein Proc-Objekt ist eine Kapselung eines Codeblocks, der in einer lokalen Variablen gespeichert, an eine Methode oder eine andere Proc übergeben und aufgerufen werden kann. Proc ist ein wesentliches Konzept in Ruby und ein Kernstück seiner funktionalen Programmierfunktionen.

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

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

Proc-Objekte sind Closures, was bedeutet, dass sie den gesamten Kontext, in dem sie erstellt wurden, speichern und verwenden können.

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

Erstellung

Es gibt mehrere Methoden, um eine Proc zu erstellen

Lambda- und Nicht-Lambda-Semantik

Procs gibt es in zwei Geschmacksrichtungen: Lambda und Nicht-Lambda (reguläre Procs). Die Unterschiede sind:

Beispiele

# +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 sind nützlich als eigenständige Funktionen, insbesondere als Argumente für Funktionen höherer Ordnung, und verhalten sich genau wie Ruby-Methoden.

Procs sind nützlich zur Implementierung von Iteratoren

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

Innerhalb von map wird der Codeblock als reguläre (Nicht-Lambda-)Proc behandelt, was bedeutet, dass die internen Arrays in Argumentpaare dekonstruiert werden und return die Methode test verlassen würde. Dies wäre mit einer strengeren Lambda nicht möglich.

Sie können eine Lambda von einer regulären Proc durch die Instanzmethode lambda? unterscheiden.

Lambda-Semantik wird typischerweise während der Lebensdauer der Proc beibehalten, einschließlich der &-Dekonstruktion in einen Codeblock

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)

Die einzige Ausnahme ist die dynamische Methodendefinition: Selbst wenn Methoden durch Übergabe einer Nicht-Lambda-Proc definiert werden, haben sie immer noch normale Semantik für die Argumentprüfung.

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

Diese Ausnahme stellt sicher, dass Methoden niemals ungewöhnliche Argumentübergabekonventionen haben, und erleichtert Wrapper, die Methoden definieren, die sich wie üblich verhalten.

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

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

Der Wrapper def2 empfängt body als Nicht-Lambda-Proc, definiert aber dennoch eine Methode mit normaler Semantik.

Konvertierung anderer Objekte in Procs

Jedes Objekt, das die Methode to_proc implementiert, kann durch den &-Operator in eine Proc konvertiert und somit von Iteratoren verwendet werden.

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!"]

Von den Ruby-Kernklassen wird diese Methode von Symbol, Method und Hash implementiert.

: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]

Verwaiste Proc

return und break in einem Block verlassen eine Methode. Wenn ein Proc-Objekt aus dem Block generiert wird und das Proc-Objekt bis zur Rückgabe der Methode überlebt, können return und break nicht funktionieren. In diesem Fall lösen return und break einen LocalJumpError aus. Ein Proc-Objekt in einer solchen Situation wird als verwaistes Proc-Objekt bezeichnet.

Beachten Sie, dass sich die Methode zum Beenden für return und break unterscheidet. Es gibt eine Situation, in der sie für break verwaist, aber nicht für return verwaist ist.

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

Da return und break in Lambdas den Block selbst verlassen, können Lambdas nicht verwaist sein.

Anonyme Blockparameter

Um das Schreiben kurzer Blöcke zu vereinfachen, bietet Ruby zwei verschiedene Arten von anonymen Parametern: it (einzelner Parameter) und nummerierte Parameter: _1, _2 und so weiter.

# 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 ist ein Name, der innerhalb eines Blocks verfügbar ist, wenn keine expliziten Parameter definiert sind, wie oben gezeigt.

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

it ist ein "Soft Keyword": Es ist kein reservierter Name und kann als Name für Methoden und lokale Variablen verwendet werden.

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

it kann auch als lokale Variable in Blöcken verwendet werden, die es als impliziten Parameter verwenden (obwohl dieser Stil offensichtlich verwirrend ist).

[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
}

In einem Block mit explizit definierten Parametern löst die Verwendung von it eine Ausnahme aus.

[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

Wenn jedoch ein lokaler Name (Variable oder Methode) verfügbar ist, wird dieser verwendet.

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

Blöcke, die it verwenden, können verschachtelt werden.

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

Blöcke, die it verwenden, werden als einen Parameter betrachtend.

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

Nummerierte Parameter

Nummerierte Parameter sind eine weitere Möglichkeit, Blockparameter implizit zu benennen. Im Gegensatz zu it ermöglichen nummerierte Parameter, sich in einem Block auf mehrere Parameter zu beziehen.

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

Parameternamen von _1 bis _9 werden unterstützt.

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

Es wird jedoch empfohlen, diese mit Bedacht zu verwenden, wahrscheinlich auf _1 und _2 und Einzeilenblöcke beschränkt.

Nummerierte Parameter können nicht zusammen mit explizit benannten verwendet werden.

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

Nummerierte Parameter können auch nicht mit it vermischt werden.

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

Um Konflikte zu vermeiden, verursacht die Benennung lokaler Variablen oder Methodenargumente als _1, _2 usw. einen Fehler.

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

Die Verwendung von impliziten nummerierten Parametern beeinflusst die Arity des Blocks.

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

Blöcke mit nummerierten Parametern können nicht verschachtelt werden.

%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 } }
#                    ^~