Herzlich willkommen zu der ersten siebten Ausgabe von ‘Ruby is Magic’. Für alle
die am 18.01. bei der colognerb nicht live mit
dabei sein konnten, oder einfach noch mal lesen wollen was passiert ist,
kommt hier nun die schriftliche Zusammenfassung mit Codestücken und
Ponies.
Inspiriert von einem hervorragenden Beitrag von Paul Cantrell, haben wir uns in dieser Folge einem Ruby-Thema gewidmet, dem jeder Ruby-Entwickler regelmäßig begegnet: Blöcke und Closures. Wie bei vielem, dass wir regelmäßig verwenden lohnt sich aber auch hier ein Blick hinter das Offensichtliche. Und vielleicht entdeckt man etwas, dass einem bisher so nicht klar war. Wir hoffen also euch neue Erkenntnisse über Closures in Ruby nahe zu bringen – wir hatten jedenfalls welche bei den Vorbereitungen der Show.
Wie so oft lassen sich Eigenschaften von Programmiersprachen am besten in der Sprache selbst verdeutlichen. Bevor wir euch aber mit einem Code-Fragment nach dem anderen bewerfen, wollen wir kurz eine Definition von Closures liefern. Sie erhebt dabei keinerlei Anspruch auf Vollständigkeit, wir brauchen nur eine gemeinsame Grundlage.
Wir verstehen Closures als Codeblöcke, die …
- zugewiesen und herumgereicht werden können.
- jederzeit und von jedem aufgerufen werden können.
- Zugriff auf Variablen im ursprünglich definierenden Kontext haben.
Diese Definition ist nicht exklusiv gültig für Ruby, sondern lässt sich
auch auf andere Sprachen anwenden. In Ruby zeichnen sich Closures
darüber hinaus dadurch aus, dass sie auf die Methode call()
antworten.
Gleichzeitig ist natürlich nicht jedes Objekt, dass auf call()
antworten kann auch ein Closure.
Die Sieben ist unsere Zahl
Wirklich bemerkenswert – und durchaus auch verwirrend – ist die Fülle an Konstrukten, die in Ruby Closures oder Closure-ähnliches beschreiben: Es gibt derer sieben an der Zahl. Auf den ersten Blick sieht das nach einer ganzen Menge aus, es stellt sich aber heraus, dass es am Ende dann doch wieder einfacher ist als gedacht.
Jetzt tun wir aber mal endlich Butter bei die Fische und schauen uns ein wenig Ruby-Code an. Den Anfang machen einfach Blöcke. Solche wie sie wohl jeder von uns täglich im Zusammenhang mit Iteratoren verwendet:
1 2 3 4 5 |
|
Der an each
übergebene Codeblock wird für jedes Element in dem Array
aufgerufen und hat dabei Zugriff auf den umgebenen Kontext. Dadurch
können wir mit @my_bag
interagieren. Damit ist eines unserer
definierenden Kriterien schon mal erfüllt.
Angenommen wir benötigen nun eine Methode each_item
auf der Klasse
Bag
die es uns ermöglicht über alles zu iterieren, was jemand dort
hineingesteckt hat. Eine mögliche Implementierung könnte dann wie folgt
aussehen:
1 2 3 4 5 6 7 8 9 |
|
Bemerkenswert an dieser Stelle ist, dass wir in der Methodensignatur
keine Parameter definiert haben. Blöcke können also einfach implizit an
Methoden weitergereicht werden. Gleichzeitig lässt sich der Block
dadurch auch nur implizit über das Schlüsselwort yield
referenzieren.
Objekte, die man yield
übergibt werden als Parameter an den Block
weiter gereicht. Falls man yield
verwendet und keinen Block übergeben
hat, beschwert sich Ruby mit der Meldung LocalJumpError: no block given (yield)
.
Ob ein Block übergeben wurde oder nicht, lässt sich mit der Methode block_given?
prüfen.
Blöcke fangen den definierenden Kontext zwar ein, können ihn jedoch (zum Glück) nicht erweitern. So erhält man dann auch bei der Ausführung des nachfolgenden Codes einen Fehler.
1 2 3 4 5 6 7 8 9 |
|
Insgesamt sind Blöcke also schon mal ein guter Schritt in die richtige Richtung. Allerdings fehlen noch zwei Eigenschaften:
- Wir können den Block nicht beliebig herumreichen (nur einmal beim Methodenaufruf) und
- Wir können den Block nicht jederzeit aufrufen.
Beiden Problemen lässt sich damit begegnen, in dem man den Block explizit macht, also in die Methodensignatur aufnimmt:
1 2 3 4 5 6 7 8 9 10 11 12 |
|
Als Nutzer der Methode ändert sich für mich dadurch übrigens überhaupt
nichts. Ich erhalte nicht einmal einen ArgumentError
wenn ich den
Block nicht an die Methode übergebe – selbst nicht bei dem Aufruf von
@items.each(&block)
, denn each
liefert einfach eine
Enumerator
-Instanz zurück, wenn kein Block mitgegeben wird.
Als letzten Schritt müssen wir nun nur noch dafür sorgen, dass der Block
gespeichert werden kann und somit jederzeit aufrufbar wird. Wir
erreichen das, indem wir uns einfach das &
-Prefix des Blockparameters
sparen, was ein Synonym für Proc.new(&block)
ist.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
|
Damit haben wir also endlich unser Closure. Wir hoffen es hat euch mal wieder gefallen und bis zum nächsten Mal … Moment! … Das kann es doch noch nicht gewesen sein. Richtig – da kommt noch was!
“Echte” Closures?!
Wie Eingangs bereits erwähnt, hält Ruby für uns mehrere Möglichkeiten bereit ein Closure zu konstruieren:
&block
ohne&
ist wieProc.new(&block)
proc {}
lambda {}
Die Variante mit proc
ist übrigens noch keinem von uns bewusst in der
freien Wildbahn aufgefallen und es handelt sich dabei auch lediglich um
ein Alias auf lambda
. Was irgendwie nicht besonders intuitiv ist. Das
hat sich dann wohl auch das Ruby-Core-Team gedacht und ab Ruby 1.9 ist
proc
sinniger Weise ein Alias auf Proc.new
.
Dennoch stellt sich die Frage, ob Unterschiede zwischen den lambda
und
Proc.new
Varianten existieren? Und wenn ja, welche? Die Antwort ist zu
erwarten gewesen: Ja, es gibt Unterschiede. Schauen wir uns einmal den Kontrollfluss und die Prüfung der Arität an.
Kontrollfluss
Wenn man innerhalb eines Closures oder Code-Blocks return
verwenden
möchte führt dies unter Umständen nicht immer zu dem gewünschten
Resultat:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
Während sich also ein return
in einem durch Proc.new
erzeugten
Closure immer auf den ursprünglich definierenden Kontext bezieht (in diesem Beispiel ist das main
), springt es in einem lambda
-Closure einfach aus dem lambda
zurück. Bei der Verwendung der lambda
-Methode erhält man also eine “true closure”, welches sich in puncto Kontrollfluss wie eine Methode verhält. In beiden Fällen erhält man übrigens eine Instanz der Proc
-Klasse.
Aritätsprüfung
Closures antworten nicht nur auf call()
, sondern auch auf die Nachricht
arity()
:
Returns the number of arguments that would not be ignored. If the block
is declared to take no arguments, returns 0. If the block is known to
take exactly n arguments, returns n. If the block has optional
arguments, return -n-1, where n is the number of mandatory arguments. A
proc with no argument declarations is the same a block declaring || as
its arguments.
Es liegt nahe, dass die Arität beim Aufruf des Closures überprüft wird.
Falls die Anzahl der Parameter dann nicht mit der erwarteten
übereinstimmt wird ein ArgumentError
geworfen. Dieses Verhalten tritt
allerdings nur auf, wenn die lambda
-Methode verwendet wurde.
“Closures” durch Proc.new
überprüfen die Arität nicht:
1 2 3 4 5 6 7 |
|
1 2 3 4 5 6 7 |
|
Aber genauso magisch wie Ruby ist, gibt ist auch immer mal wieder
Momente der Verwirrung.
So auch in diesem Fall: Closures aus lambda
prüfen die Arität nur in
Ruby 1.9 so wie erwartet. Und damit kommen wir dann auch zum Fun Fact
dieser Ausgabe:
In Ruby 1.8 gilt für Closures durch lambda
:
lambda {||}.arity != lambda {}.arity
lambda {}.arity == -1
- Die Anzahl der Argumente wird nicht geprüft wenn die Arität 1 ist
In Ruby 1.9 ist die Welt aber wie gesagt in Ordnung, zumindest in dieser
Hinsicht: lambda {}.arity == lambda {||}.arity == 0
Dirk hat ein nettes Beispiel für Closures in seinem Blog vor einiger Zeit geschrieben: Roll your own lazy loading collection beschreibt, wie man eine lazy collection bauen kann.
One More Thing
Fassen wir einmal zusammen, welche Möglichkeiten von Closures wir bisher besprochen haben:
- block (implizit übergeben)
- block (explizit übergeben)
- block (explizit übergeben und zu Proc)
Proc.new
proc
(Alias auflambda
/Proc.new
)lambda
Aber fehlt da nicht noch etwas? JA!
Methoden sind ebenfalls Closures! Das sie den umgebenen Kontext einfangen ist offensichtlich. Sie überprüfen weiterhin die Arität und ein return
springt nur aus der Methode heraus.
Um eine Methode referenzierbar zu machen, benötigt man die method()
-Methode. Mit method()
erhält man eine Method
-Instanz, welches die Methode repräsentiert.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
|
Als Beispiel für Methoden als Closures haben wird eine Implementierung von Python-style Decorators gezeigt. Den Code dazu findet ihr hier und ein paar weitere Details wird es in einem separatem Post geben.
Präsentation
Und hier noch die Präsentation. Für das volle audio-visuelle Erlebnis müsst ihr allerdings zur @colognerb kommen ;-)