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.hour erhä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 Beispiel wichtig soll 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 wiederum log aufruft 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...