Ruby, Currying und Block-Vereinfachung

Der für mich faszinierendste Aspekt funktionaler Programmierung ist das Currying. Es erlaubt das Erzeugen neuer Funktionen, indem ein oder mehrere Argumente einer Ausgangsfunktion vorbelegt werden. Auch in der objektorientierten Entwicklung bieten sich solche Ansätze an. Mir fällt dies immer dann auf, wenn ich in Ruby bei einem Array#map einen Block angeben muss und das Gefühl habe, dass das auch einfacher sein könnte. Ein Stück weit kommt Ruby mir dabei entgegen.

Symbol#to_proc

Ein einfacher Fall: Auf jedem Element einer Liste soll eine argumentlose Methode aufgerufen werden:

labels = [ 'Home', 'Gallery', 'Contact' ]
puts labels.map { |label| label.upcase }.join(', ') 
# => HOME, GALLERY, CONTACT

Alternativ lässt sich der Block als Proc realisieren und mit dem Kaufmannsund voran als Argument übergeben – in diesem Fall versteht Ruby das Argument nicht als solches und nutzt es als Block:

upcase_proc = ->(label) { label.upcase }
puts labels.map(&upcase_proc).join(', ')
# => HOME, GALLERY, CONTACT

Ist dieses &-Argument kein Proc, so wird, wenn vorhanden, dessen Methode to_proc aufgerufen. Praktischerweise implementiert die Klasse Symbol diese Methode. Das erzeugte Proc nimmt ein Objekt als Argument und ruft dessen Send-Methode mit dem Symbol als erstem Argument auf. Dies erlaubt die folgende Notation:

puts labels.map(&:upcase).join(', ')
# => HOME, GALLERY, CONTACT

Object#method

Dies hilft natürlich nicht, wenn in dem Block ein weiteres Argument benötigt wird:

puts labels.select { |label| label != 'Home' }.join(', ') 
# => Gallery, Contact

In diesem Fall hilft Object#method. Es macht aus einer Methode ein Proc, self bleibt dabei erhalten. "Foo".method(:eql?) liefert ein in ein Proc umwandelbares Method-Objekt, das ein Argument nimmt und dieses auf Gleichheit mit dem String „Foo“ testet. Für das Beispiel ermöglicht dies

puts labels.select(&'Home'.method(:!=)).join(', ')
# => Gallery, Contact

Leider funktioniert dieser Ansatz nur, wenn man Objekt und Argument vertauschen kann. Im Fall von Vergleichsoperatoren ist dies kein Problem.

Proc#curry

Bisher ging alles noch ohne Currying. Schon das letzte Beispiel war etwas kürzer als die Blockvariante, die Lesbarkeit wurde jedoch schlechter. Beim Currying wird Ruby nicht besser, auch wenn Procs dies von Haus aus unterstützen.

Das Beispiel ähnelt dem vorigen, nur dass das Proc neben dem Element aus dem Array noch ein weiteres Argument benötigt:

puts labels.map { |label| 'Click %'.gsub('%', label) }.join(', ')
# => Click Home, Click Gallery, Click Contact

Das von Object#method zurückgegebene Method-Objekt ist kein Proc und muss daher manuell umgeformt werden, bevor curry verwendet werden kann. Die Methode nimmt dann als einziges Argument die Anzahl der Argumente. Anschließend kann man mit eckigen Klammern Argumente vorbelegen und neue Procs erzeugen:

puts labels.map(&'Click %'.method(:gsub).to_proc.curry(2)['%']).join(', ') 
# => Click Home, Click Gallery, Click Contact

Diese Variante ist länger und nur mit Mühe zu verstehen. Will man solche funktionalen Ansätze in Ruby verwenden, wird man sich daher zunächst den einen oder anderen Helfer schreiben müssen.