Blocks und Procs
Jetzt kommen wir zu einer der coolsten Möglichkeiten in Ruby. Wenige andere Sprachen kennen dieses Feature, die meisten bezeichnen es anders (wie etwa closures), aber die meisten verbreiteten kennen es nicht, und das ist eine Schande!
Also, was ist das für ein cooles neues Ding?
Es handelt sich um die Möglichkeit,
einen Block Programm-Code
(also Code zwischen einem do und einem end),
ihn in ein Objekt einzupacken und in einer Variable zu speichern
oder ihn einer Methode zu übergeben,
ihn an beliebigen Stellen auszuführen, wo auch immer du willst
(auch mehrfach, wenn du willst).
Damit ist der Block selber so eine Art Methode, außer dass er nicht an ein Object gebunden ist
(sondern selbst ein Objekt ist) und man kann ihn behandeln wie jedes andere Objekt auch.
Es ist an der Zeit für ein Beispiel:
Trinkspruch = Proc.new do puts 'Prost!' end Trinkspruch.call Trinkspruch.call Trinkspruch.call
Prost! Prost! Prost!
Hier habe ich einen Proc (was wohl eine Abkürzung für Procedure/Prozedur sein soll, aber, weitaus wichtiger, sich auf Block reimt) der den Programm-Block enthielt, den ich darauhin dreimal aufgerufen habe. Wie du siehst ist es eine Methode recht ähnlich.
Es gleicht sogar noch mehr einer Methode, denn Blocks können auch Parameter annehmen:
magstDu = Proc.new do |leckerei| puts 'ich *liebe* '+leckerei+'!!!' end magstDu.call 'Schokolade' magstDu.call 'Prosecco'
ich *liebe* Schokolade!!! ich *liebe* Prosecco!!!
Wir sehen also, was Blocks und Procs sind und wie man sie benutzt, aber was soll das alles? Warum verwenden wir nicht einfach Methoden? Nun, es gibt eben ein paar Dinge, die man mit Methoden nicht machen kann. Insbesondere kannst du einer Methode keine andere Methode übergeben (aber man kann Procs an Methoden übergeben) und Methoden können keine anderen Methoden zurückgeben (aber sie können Procs zurückgeben). Das ist ganz einfach und logisch, denn Procs sind Objekte, Methoden nicht.
Übrigens: schaut das irgendwie bekannt aus? Ja, du hast Blocks schon kennengelernt... als du über Iteratoren gelernt hast. Aber darüber werden wir ein bisschen später sprechen.)
Methoden können Procs übernehmen
Wenn wir einen Proc an eine Methode übergeben, können wir steuern, wie, ob oder wie of der Proc aufgerufen werden soll. Angenommen, wir wollen etwas spezielles tun, bevor und nachdem der eigentliche Programm-Code ausgeführt wird:
def wichtig eineProc puts 'Alles ruhig halten! Ich muss etwas wichtiges machen...' eineProc.call puts 'OK, hat sich erledigt, ich bin fertig. Ihr könnt weitermachen.' end begruessen = Proc.new do puts 'hallo' end verabschieden = Proc.new do puts 'servus' end wichtig begruessen wichtig verabschieden
Alles ruhig halten! Ich muss etwas wichtiges machen... hallo OK, hat sich erledigt, ich bin fertig. Ihr könnt weitermachen. Alles ruhig halten! Ich muss etwas wichtiges machen... servus OK, hat sich erledigt, ich bin fertig. Ihr könnt weitermachen.
Das mag jetzt nicht besonders großartig erscheinen, ist es aber tatsächlich ;-) In der Programmierung sind strikte Regel üblich, wann was zu tun sei. Wenn du zum Beispiel in eine Datei speichern willst, so musst du sie öffnen, die Information hineinschreiben und die Datei wieder schließen. Wenn du vergisst, sie zu schliessen, können schlimme Dinge passieren. Aber jedesmal, wenn du etwas mit einer Datei machen will, muss du diese 3 Schritte abarbeiten: öffnen, das, was du eigentlich machen willst, und wieder schließen. Das ist nervig und kann leicht vergessen werden. In Ruby wird das Speichern (oder Laden) einer Datei analog zu dem obigen Code abgearbeitet und du musst dir überhaupt keine weiteren Gedanken machen als über das, was du eigentlich speichern willst (oder laden). (Im folgenden Abschnitt werde ich dir zeigen, wo du Information über das Laden und Speichern von Dateien findest.)
Du kannst auch Methoden schreiben, die selbst entscheiden ob und wie oft eine Proc ausgeführt werden soll. Hier ist ein Beispiel für eine Methode, die in 50% der Fälle die Proc ausführt. Die andere Methode führt die Proc 2-mal aus.
def vielleicht eineProc
if rand(2) == 0
eineProc.call
end
end
def zweimal eineProc
eineProc.call
eineProc.call
end
rechts = Proc.new do
puts 'rechts '
end
links = Proc.new do
puts 'links '
end
vielleicht rechts
vielleicht links
zweimal rechts
zweimal links
rechts links rechts rechts links links
Hier stelle ich ein paar verbreitete Verwendungsmöglichkeiten von Procs vor,
die wir mit Methoden alleine einfach nicht hätten.
An diesem Beispiel sieht man recht gut, wie sehr eine Methode von der Proc abhängen kann,
die ihr übergeben wird.
Unsere Methode übernimmt ein Objekt und ein eProc und ruft die Proc auf dem Objekt auf.
Wenn die Proc false zurückgibt, beenden wir die Methode;
ansonsten rufen wir die Proc mit dem zurückgegebenen Objekt noch einmal auf.
Das machen wir so lange, bis die Proc false zurückgibt
(was sie irgendwann tun sollte, da ansonsten das Programm abstürzt).
Unsere Methode gibt den letzten nicht-false-Wert zurück,
den es von der Proc erhalten hatte.
def bisFalsch einObjekt, eineProc
eingabe =einObjekt
ausgabe =einObjekt
while ausgabe
eingabe =ausgabe
ausgabe =eineProc.call eingabe
end
eingabe
end
quadrateArray = Proc.new do |array|
letzte = array.last
if letzte <=0
false
else
array.pop
array.push letzte*letzte
array.push letzte-1
end
end
immerFalsch = Proc.new do |ignorieren|
false
end
puts bisFalsch([13], quadrateArray).inspect
puts bisFalsch('ich sitze hier mitten in der Nacht und programmiere: es macht Spaß!', immerFalsch)
[169, 144, 121, 100, 81, 64, 49, 36, 25, 16, 9, 4, 1, 0] ich sitze hier mitten in der Nacht und programmiere: es macht Spaß!
Ok, ich gebe zu, das war ein ziemlich komisches Beispiel. Aber es zeigt, wie unterschiedlich unsere Methode sich verhalten kann mit verschiedenen Procs.
Die verwendete inspect-Methode ist der Methode to_s recht ähnlich,
außer dass der zurückgegebene String noch versucht den Ruby-Code darzustellen,
den man zum Bauen des Objektes verwendet hat.
Hier zeigt es uns das ganze Array, das beim ersten Aufruf von bisFalsch erzeugt worden ist.
Vielleicht ist dir aufgefallen, dass wir nie das Quadrat von 0 gebildet haben,
aber da das Quadrat von 0 wieder 0 ergibt, mussten wir auch nicht.
Und weil immerFalsch immer false ergab,
hat bisFalsch die Methode nur einmal aufgerufen
und nur das Objekt zurückgegeben, was wir übergeben hatten.
Methoden, die Procs zurückgeben
Eine weitere coole Sache, die du mit Procs machen kannst, ist, sie in einer Methode herzustellen und anschließend zurückzugeben. Das ermöglicht uns verschiedene Varianten verrückter Programmierung (Viele Sachen mit beeindruckenden Namen wie lazy evaluation, unendliche Datenstruktuen und Currying), aber in der Praxis verwende ich dies fast nie, noch kenne ich jemanden, der das macht. Ich denke, es ist nicht das Übliche, was man als Ruby-Programmierer tagtäglich einsetzt, oder aber Ruby bietet viele Möglichkeiten für andere Lösungen. Ich bin mir da nicht sicher. Auf jeden Fall werde ich dies nur kurz ansprechen.
Die Methode zusammen in diesem Beispiel nimmt zwei Procs
und gibt eine neue zurück, die erst die erste Proc aufruf und das Ergebnis
an die zweite Proc weitergibt.
def zusammen proc1, proc2
Proc.new do |x|
proc2.call(proc1.call(x))
end
end
weiter = Proc.new do |x|
x+1
end
quadrat = Proc.new do |x|
x*x
end
weiter_dann_quadrat = zusammen weiter, quadrat
quadrat_dann_weiter = zusammen quadrat, weiter
puts weiter_dann_quadrat.call(5)
puts quadrat_dann_weiter.call(5)
36 26
Man beachte, dass der Aufruf von proc1 in Klammern stehen muss,
um zuerst ausgeführt zu werden.
Blocks (nicht Procs!) an Methoden übergeben
OK, das war nun von akademischem Interesse und auch etwas umständlich zu verwenden. Das größte Problem besteht darin, dass man drei Schritte abarbeiten muss, (Definieren der Methode, Erzeugen der Proc und Aufruf der Methode mit der Proc) obwohl man das Gefühl hat, es sollten nur zwei sein (Definieren der Methode und Übergeben des Blocks in die Methode, ohne überhaupt eine Proc zu verwenden), weil man in der Regel den Block (die Proc) sowieso nicht mehr verwendet, nachdem man ihn der Methode übergeben hat. Kaum zu glauben, die Macher von Ruby hatten diese Idee auch! Tatsächlich hast du das auch schon gemacht, jedesmal wenn wir Iteratoren verwendet haben.
Schauen wir uns ein Beispiel an, dann reden wir darüber.
class Array
def eachEven(&einBlock)
gerade=true
self.each do |obj|
if gerade
einBlock.call obj
end
gerade = (not gerade)
end
end
end
['Apfel', 'Pferdeapfel', 'Pflaumen', 'Dattel'].eachEven do |obst|
puts 'Lecker! Ich liebe '+obst+'-Kuchen! Du nicht?'
end
# Zur Erinnerung: mit eachEven bekommen wir die geraden Elemente
# des Arrays, die hier zufällig die ungeraden Zahlen sind...
[1,2,3,4,5].eachEven do |ungerade|
puts ungerade.to_s + ' ist keine gerade Zahl!'
end
Lecker! Ich liebe Apfel-Kuchen! Du nicht? Lecker! Ich liebe Pflaumen-Kuchen! Du nicht? 1 ist keine gerade Zahl! 3 ist keine gerade Zahl! 5 ist keine gerade Zahl!
Um also einen Block an eachEven zu übergeben,
mussten wir den Block lediglich an die Methode anhängen.
Auf diese Art kann man einen Block an jeder Methode übergeben,
obwohl die meisten Methoden den Block einfach ignorieren werden.
Um zu bewirken, dass deine eigene Methode den Block nicht ignoriert,
nimm ihn, verwandle ihn in eine Proc
und hänge den Namen der Proc an die Parameterliste der Methode an,
mit einem vorangestellten &-Zeichen.
Dies ist ein bisschen trickreich, aber nur ein bisschen,
und du musst es auch nur einmal bei der Methodendeklaration machen.
Danach kannst du die Methode immer wieder mit einem Block verwenden,
genauso wie die vordefinierten Methoden,
die Blocks übernehmen, wie each oder times.
(Du erinnerst dich an 5.times do ..., oder?)
Wenn es dich verwirrt, dann erinnere dich daran, wie eachEven
funktioniert: ruf den Block auf mit jedem zweiten Element aus dem Array.
Einmal richtig geschrieben, musst du nicht mehr wissen, wie es funktioniert.
Du musst auch nicht mehr darüber nachdenken, waĆ©lcher Block wann aufgerufen wird.
Und das ist genau der Grund, warum wir Methoden dieser Art schreiben:
wir müssen nie wieder darüber nachdenken, wie sie arbeiten...
wir verwenden sie einfach.
Ich weiß noch genau, wie ich einmal eine Möglichkeit brauchte, um zu messen, wie lange einzelne Bereiche eines Programms bei der Ausführung brauchen. (Dies wird auch als Profiling des Codes bezeichnet.) Also habe ich eine Methode geschrieben, die einen Block übernimmt, die Zeit vor Abarbeitung des Blocks speichert, den Block abarbeitet, die Zeit wieder nimmt und die Differenz berechnet. Ich finde den Code im Moment nicht, aber er sah in etwa so aus:
def profile beschreibung, &block
start = Time.now
block.call
ende = Time.now
dauer = ende - start
puts beschreibung+': '+dauer.to_s+' Sekunden'
end
profile '25000 Verdoppelungen' do
nummer =1
25000.times do
nummer = nummer+nummer
end
puts nummer.to_s.length.to_s+ 'Ziffern' # Anzahl der Ziffern in dieser großen Zahl
end
profile 'Zählen bis zu 1 Million' do
nummer =0
1000000.times do
nummer = nummer+1
end
end
7526Ziffern 25000 Verdoppelungen: 0.358207 Sekunden Zählen bis zu 1 Million: 2.107091 Sekunden
Wie einfach und elegant!
Mit dieser kleinen Methode kann ich nun ganz schnell jeden Programmabschnitt mal eben messen.
Ich packe den Code nur in einen Block und sende ihn an die Methode profile.
Geht's einfacher?
In den meisten Programmiersprachen müsste ich an jeder Stelle haufenweise Code zur
Zeitmessung einfügen (was wir hier alles in profile haben).
In Ruby kann man es an einer Stelle schreiben und (besonders wichtig) aus dem anderen Code raushalten!
Selber ausprobieren
- Großvaters Uhr:
Schreib eine Methode, die einen Block übernimmt und diesen für jede Stunde, die
am heutigen Tage bereits verstrichen ist, einmal aufruft.
Also, wenn man dem Programm einen Block übergibt, der "DingDong" ausgibt,
so schlägt es die aktuelle Stunde, wie die Uhr bei meinem Großvater...
oder zumindest so ähnlich.
Teste dein Programm mit mehreren verschiedenen Blöcken
(z.B. den DingDong-Block, den ich eben beschrieben habe).
Tip: mit
Time.now.hourerhältst du die aktuelle Stunde, allerdings als eine Zahl zwischen 0 und 23. Du musst selber rumrechnen, dass du die normale Stundenzahl (1-12) hast. - Programm Logbuch:
Schreib eine Methode
log, die eine Beschreibung eines Blocks und -natürlich- einen Block übernimmt. Ähnlich wie im Beispielwichtigsoll sie einen String ausgeben, mit dem sie mitteilt, dass sie den Block nun startet, einen anderen String am Ende, sowie die Dauer und das Ergebnis des Blocks. Teste deine Methode mit einem Block, innerhalb dem du wiederumlogaufruft und einen anderen Block übergibst. (Dies wird als Verschachtelung bezeichnet.) Lass dich von der Ausgabe überraschen. - Verbessertes Logbuch:
Die Ausgabe vom letzten Logger war relativ schwer zu lesen und es wurde immer schlimmer,
je tiefer man den Aufruf des Loggers schachtelte.
Es wäre so viel einfacher zu lesen, wenn es den inneren Block einrücken würde.
Um dies zu erreichen, muss man mitzählen, wie tief man verschachtelt ist und entsprechend
viele Leerzeichen voranstellen wann immer der Logger etwas schreiben will.
Dafür verwendet man ein sogenannte globale Variable. Das ist eine Variable,
die überall im Code sichtbar ist.
Um eine globale Variable zu definieren stelle dem Variablennamen ein
$voran, wie etwa,und. Die Ausgabe soll dann in etwa so aussehen:START "äußerer Block" START "kleiner Block" START "kleinerer Block" ENDE "kleinerer Block" Dauer:0.001379 Ergebnis: 40320 ENDE "kleiner Block" Dauer: 0.001493 Ergebnis: gute Nacht ENDE "äußerer Block" Dauer: 0.001634 Ergebnis: false
Das war nun alles, was du in diesem Tutorial lernen wirst. Glückwunsch!!! Du hast unheimlich viel gelernt! Vielleicht erinnerst du dich nicht an alles oder du hast ein paar Abschnitte schnell überlesen... das ist völlig in Ordnung! Beim Programmieren geht es nicht darum, was du weißt, sondern darum, was du herausfinden kannst. Solange du weißt, wo du nachlesen kannst, was du vergessen hast, bist du im grünen Bereich. I hoffe, du denkst nicht, dass ich all dies hier geschrieben habe, ohne mal hier und da nachzusehen. Natürlich habe ich nachgeschlagen. Und ich bekam viel Unterstützung bei dem Code, der dieses Tutorial (in der englischen Fassung!) enthält. Aber wo genau schau ich nach, wenn ich Hilfe brauche? Im letzten Kapitel bekommst du ein paar Tips...