10. Blöcke und Procs

Dies ist definitiv eines der coolsten Features von Ruby. Einige andere Sprachen haben dieses Feature, obwohl sie es vielleicht anders nennen (wie Closures), aber viele der beliebteren Sprachen haben es nicht, was eine Schande ist.

Was ist also dieses coole neue Ding? Es ist die Fähigkeit, einen Codeblock (Code zwischen do und end) zu nehmen, ihn in ein Objekt (genannt Proc) zu verpacken, ihn in einer Variablen zu speichern oder an eine Methode zu übergeben und den Code im Block auszuführen, wann immer Sie wollen (mehr als einmal, wenn Sie wollen). Es ist also eine Art Methode, außer dass sie nicht an ein Objekt gebunden ist (es ist ein Objekt), und Sie können es speichern oder herumreichen wie jedes andere Objekt. Ich denke, es ist Zeit für ein Beispiel:

prost = Proc.new do
  puts 'Prost!'
end

prost.call
prost.call
prost.call

Ich habe eine Proc erstellt (ich glaube, das ist die Abkürzung für "Prozedur", aber das Wichtigste ist, dass es sich auf "Block" reimt), die einen Codeblock enthält, und dann habe ich die Proc dreimal aufgerufen (call). Wie Sie sehen können, sieht es einer Methode sehr ähnlich.

Eigentlich ist es sogar noch mehr wie eine Methode, als ich Ihnen gezeigt habe, denn Blöcke können Parameter annehmen:

magstDu = Proc.new do |eineGuteSache|
  puts 'Ich mag '+eineGuteSache+' *wirklich*!'
end

magstDu.call 'Schokolade'
magstDu.call 'Ruby'

Okay, wir sehen also, was Blöcke und Procs sind und wie man sie benutzt, aber was ist der Sinn? Warum nicht einfach Methoden verwenden? Nun, weil es einige Dinge gibt, die man mit Methoden einfach nicht machen kann. Insbesondere können Sie keine Methoden an andere Methoden übergeben (aber Sie können Procs an Methoden übergeben), und Methoden können keine anderen Methoden zurückgeben (aber sie können Procs zurückgeben). Das liegt einfach daran, dass Procs Objekte sind; Methoden nicht.

(Übrigens, kommt Ihnen das bekannt vor? Ja, Sie haben Blöcke schon einmal gesehen... als Sie etwas über Iteratoren gelernt haben. Aber lassen Sie uns gleich mehr darüber sprechen.)

Methoden, die Procs annehmen

Wenn wir eine Proc an eine Methode übergeben, können wir steuern, wie, ob oder wie oft wir die Proc aufrufen. Nehmen wir zum Beispiel an, es gibt etwas, das wir vor und nach der Ausführung von Code tun wollen:

def machWichtigeSache eineProc
  puts 'Alle mal WARTE! Ich muss etwas erledigen...'
  eineProc.call
  puts 'Ok Leute, ich bin fertig. Weitermachen.'
end

sagHallo = Proc.new do
  puts 'hallo'
end

sagTschuess = Proc.new do
  puts 'tschuess'
end

machWichtigeSache sagHallo
machWichtigeSache sagTschuess

Vielleicht klingt das nicht so fabelhaft... aber das ist es. :-) Es ist beim Programmieren sehr üblich, strenge Anforderungen an Dinge zu haben, die getan werden müssen. Wenn Sie zum Beispiel eine Datei speichern wollen, müssen Sie die Datei öffnen, die gewünschten Informationen hineinschreiben und die Datei dann schließen. Wenn Sie vergessen, die Datei zu schließen, können Schlimme Dinge(tm) passieren. Aber jedes Mal, wenn Sie eine Datei speichern oder laden wollen, müssen Sie dasselbe tun: die Datei öffnen, das tun, was Sie wirklich tun wollen, und sie dann schließen. Das ist mühsam und leicht zu vergessen. In Ruby funktioniert das Speichern (oder Laden) von Dateien ähnlich wie der obige Code, sodass Sie sich um nichts anderes kümmern müssen als um das, was Sie speichern (oder laden) wollen. (Im nächsten Kapitel zeige ich Ihnen, wie man Dinge wie das Speichern und Laden von Dateien macht.)

Sie können auch Methoden schreiben, die bestimmen, wie oft oder ob überhaupt eine Proc aufgerufen wird. Hier ist eine Methode, die eine Proc etwa die Hälfte der Zeit aufruft, und eine andere, die sie zweimal aufruft:

def vielleichtMachen eineProc
  if rand(2) == 0
    eineProc.call
  end
end

def machZweimal eineProc
  eineProc.call
  eineProc.call
end

zwinkern = Proc.new do
  puts '<zwinkern>'
end

blick = Proc.new do
  puts '<blick>'
end

vielleichtMachen zwinkern
vielleichtMachen blick
machZweimal zwinkern
machZweimal blick

(Wenn Sie diese Seite ein paar Mal neu laden, werden Sie sehen, dass sich die Ausgabe ändert.) Dies sind einige der häufigsten Anwendungen von Procs, die es uns ermöglichen, Dinge zu tun, die wir mit Methoden allein einfach nicht tun könnten. Sicher, Sie könnten eine Methode schreiben, die zweimal zwinkert, aber Sie könnten keine schreiben, die einfach irgendetwas zweimal tut!

Bevor wir weitermachen, lassen Sie uns noch ein letztes Beispiel betrachten. Bisher waren die Procs, die wir übergeben haben, einander ziemlich ähnlich. Diesmal werden sie ziemlich unterschiedlich sein, damit Sie sehen können, wie sehr eine Methode von den ihr übergebenen Procs abhängt. Unsere Methode nimmt ein Objekt und eine Proc und ruft die Proc auf diesem Objekt auf. Wenn die Proc false zurückgibt, beenden wir; andernfalls rufen wir die Proc mit dem zurückgegebenen Objekt auf. Wir machen das so lange, bis die Proc false zurückgibt (was sie besser irgendwann tun sollte, oder das Programm stürzt ab). Die Methode gibt den letzten nicht-falschen Wert zurück, der von der Proc zurückgegeben wurde.

def machBisFalsch ersteEingabe, eineProc
  eingabe = ersteEingabe
  ausgabe = ersteEingabe

  while ausgabe
    eingabe = ausgabe
    ausgabe = eineProc.call eingabe
  end

  eingabe
end

baueQuadratArray = Proc.new do |array|
  letzteZahl = array.last
  if letzteZahl <= 0
    false
  else
    array.pop                         # Nimm die letzte Zahl weg...
    array.push letzteZahl*letzteZahl  # ...und ersetze sie durch ihr Quadrat...
    array.push letzteZahl-1           # ...gefolgt von der nächstkleineren Zahl.
  end
end

immerFalsch = Proc.new do |ignorierMichEinfach|
  false
end

puts machBisFalsch([5], baueQuadratArray).inspect
puts machBisFalsch('Ich schreibe das um 3:00 Uhr morgens; jemand soll mich ausknocken!', immerFalsch)

Okay, das war ein ziemlich seltsames Beispiel, ich gebe es zu. Aber es zeigt, wie unterschiedlich unsere Methode reagiert, wenn ihr verschiedene Procs gegeben werden.

Die Methode inspect ist to_s sehr ähnlich, außer dass der zurückgegebene String versucht, Ihnen den Ruby-Code zum Erstellen des übergebenen Objekts zu zeigen. Hier zeigt er uns das gesamte Array, das von unserem ersten Aufruf von machBisFalsch zurückgegeben wurde. Vielleicht bemerken Sie auch, dass wir die 0 am Ende dieses Arrays nie quadriert haben, aber da 0 im Quadrat immer noch nur 0 ist, mussten wir das nicht. Und da immerFalsch bekanntlich immer falsch war, hat machBisFalsch beim zweiten Aufruf überhaupt nichts getan; es hat einfach das zurückgegeben, was übergeben wurde.

Methoden, die Procs zurückgeben

Eines der anderen coolen Dinge, die Sie mit Procs machen können, ist, sie in Methoden zu erstellen und sie zurückzugeben. Dies ermöglicht alle möglichen verrückten Programmierkräfte (Dinge mit beeindruckenden Namen wie Lazy Evaluation, unendliche Datenstrukturen und Currying), aber Tatsache ist, dass ich das in der Praxis fast nie tue, und ich erinnere mich auch nicht daran, dass jemand anderes das in seinem Code getan hat. Ich denke, es ist die Art von Sache, die man in Ruby einfach nicht macht, oder vielleicht ermutigt Ruby einen einfach dazu, andere Lösungen zu finden; ich weiß es nicht. Jedenfalls werde ich das nur kurz anreißen.

In diesem Beispiel nimmt komponieren zwei Procs und gibt eine neue Proc zurück, die beim Aufruf die erste Proc aufruft und ihr Ergebnis an die zweite Proc übergibt.

def komponieren proc1, proc2
  Proc.new do |x|
    proc2.call(proc1.call(x))
  end
end

quadrat = Proc.new do |x|
  x * x
end

verdoppeln = Proc.new do |x|
  x + x
end

verdoppelnDannQuadrat = komponieren verdoppeln, quadrat
quadratDannVerdoppeln = komponieren quadrat, verdoppeln

puts verdoppelnDannQuadrat.call(5)
puts quadratDannVerdoppeln.call(5)

Beachten Sie, dass der Aufruf von proc1 innerhalb der Klammern für proc2 sein musste, damit er zuerst ausgeführt wird.

Übergeben von Blöcken (nicht Procs) an Methoden

Okay, das war also irgendwie akademisch interessant, aber auch ein bisschen mühsam zu benutzen. Ein großer Teil des Problems ist, dass es drei Schritte gibt, die Sie durchlaufen müssen (Methode definieren, Proc erstellen und Methode mit der Proc aufrufen), wenn es sich anfühlt, als sollten es nur zwei sein (Methode definieren und den Block direkt in die Methode übergeben, ohne überhaupt eine Proc zu verwenden), da Sie die Proc/den Block meistens nicht verwenden wollen, nachdem Sie sie an die Methode übergeben haben. Nun, wer hätte es gedacht, Ruby hat das alles für uns geregelt! Tatsächlich haben Sie das schon jedes Mal getan, wenn Sie Iteratoren verwendet haben.

Ich zeige Ihnen ein schnelles Beispiel, und dann reden wir darüber.

class Array

  def jedesGerade(&warEinBlock_jetztEineProc)
    istGerade = true  # Wir beginnen mit "true", da Arrays bei 0 beginnen, was gerade ist.

    self.each do |objekt|
      if istGerade
        warEinBlock_jetztEineProc.call objekt
      end

      istGerade = !istGerade  # Umschalten von gerade zu ungerade oder ungerade zu gerade.
    end
  end

end

['Apfel', 'fauler Apfel', 'Kirsche', 'Durian'].jedesGerade do |frucht|
  puts 'Lecker! Ich liebe '+frucht+'-Kuchen einfach, du nicht?'
end

# Denken Sie daran, wir bekommen die gerade nummerierten Elemente
# des Arrays, die zufällig alle ungerade Zahlen sind,
# nur weil ich gerne solche Probleme verursache.
[1, 2, 3, 4, 5].jedesGerade do |komischerVogel|
  puts komischerVogel.to_s+' ist KEINE gerade Zahl!'
end

Um einen Block an jedesGerade zu übergeben, mussten wir nur den Block hinter die Methode kleben. Sie können einen Block auf diese Weise an jede Methode übergeben, obwohl viele Methoden den Block einfach ignorieren. Damit Ihre Methode den Block nicht ignoriert, sondern ihn greift und in eine Proc verwandelt, setzen Sie den Namen der Proc an das Ende der Parameterliste Ihrer Methode, vorangestellt von einem kaufmännischen Und (&). Dieser Teil ist ein bisschen knifflig, aber nicht allzu schlimm, und Sie müssen es nur einmal tun (wenn Sie die Methode definieren). Dann können Sie die Methode immer und immer wieder verwenden, genau wie die eingebauten Methoden, die Blöcke annehmen, wie each und times. (Erinnern Sie sich an 5.times do...?)

Wenn Sie verwirrt sind, denken Sie einfach daran, was jedesGerade tun soll: den übergebenen Block für jedes zweite Element im Array aufrufen. Sobald Sie es geschrieben haben und es funktioniert, müssen Sie nicht mehr darüber nachdenken, was es eigentlich unter der Haube tut ("welcher Block wird wann aufgerufen??"); tatsächlich ist das genau der Grund, warum wir Methoden wie diese schreiben: damit wir nie wieder darüber nachdenken müssen, wie sie funktionieren. Wir benutzen sie einfach.

Ich erinnere mich, dass ich einmal messen wollte, wie lange verschiedene Abschnitte meines Codes dauerten (dies wird als Profiling des Codes bezeichnet). Also schrieb ich eine Methode, die die Zeit vor der Ausführung des Codes nimmt, ihn ausführt, die Zeit am Ende wieder nimmt und die Differenz zurückgibt. Ich kann den Code gerade nicht finden, aber ich brauche ihn nicht; er sah wahrscheinlich ungefähr so aus:

def profil blockBeschreibung, &block
  startZeit = Time.now

  block.call

  dauer = Time.now - startZeit

  puts blockBeschreibung+': '+dauer.to_s+' Sekunden'
end

profil '25000 Verdopplungen' do
  nummer = 1

  25000.times do
    nummer = nummer + nummer
  end

  puts nummer.to_s.length.to_s+' Ziffern'  # Das ist die Anzahl der Ziffern in dieser RIESIGEN Zahl.
end

profil 'zaehle bis eine Million' do
  nummer = 0

  1000000.times do
    nummer = nummer + 1
  end
end

Wie einfach! Wie elegant! Mit dieser winzigen Methode kann ich jetzt ganz einfach jeden Abschnitt eines jeden Programms messen, das ich will; ich werfe den Code einfach in einen Block und sende ihn an profil. Was könnte einfacher sein? In den meisten Sprachen müsste ich diesen Zeitmessungscode (das Zeug innerhalb von profil) explizit um jeden Abschnitt hinzufügen, den ich messen wollte. In Ruby kann ich jedoch alles an einem Ort halten und (was noch wichtiger ist) mir aus dem Weg räumen!

Ein paar Dinge zum Ausprobieren

  • Standuhr. Schreiben Sie eine Methode, die einen Block nimmt und ihn einmal für jede Stunde aufruft, die heute vergangen ist. Auf diese Weise, wenn ich den Block do puts 'DONG!' end übergeben würde, würde sie (sozusagen) wie eine Standuhr läuten. Testen Sie Ihre Methode mit ein paar verschiedenen Blöcken (einschließlich dem, den ich Ihnen gerade gegeben habe). Hinweis: Sie können Time.now.hour verwenden, um die aktuelle Stunde zu erhalten. Dies gibt jedoch eine Zahl zwischen 0 und 23 zurück, also müssen Sie diese Zahlen ändern, um gewöhnliche Zifferblattzahlen (1 bis 12) zu erhalten.
  • Programm-Logger. Schreiben Sie eine Methode namens log, die eine String-Beschreibung eines Blocks und natürlich einen Block annimmt. Ähnlich wie machWichtigeSache sollte sie einen String ausgeben (puts), der besagt, dass der Block begonnen hat, und einen weiteren String am Ende, der Ihnen sagt, dass der Block beendet ist, und Ihnen auch sagt, was der Block zurückgegeben hat. Testen Sie Ihre Methode, indem Sie ihr einen Codeblock senden. Innerhalb des Blocks setzen Sie einen weiteren Aufruf von log und übergeben ihm einen weiteren Block. (Das nennt man Verschachtelung.) Mit anderen Worten, Ihre Ausgabe sollte ungefähr so aussehen:
    Beginne "aeusserer Block"...
    Beginne "ein kleiner Block"...
    ..."ein kleiner Block" beendet, Rueckgabe:  5
    Beginne "noch ein anderer Block"...
    ..."noch ein anderer Block" beendet, Rueckgabe:  Ich mag thailaendisches Essen!
    ..."aeusserer Block" beendet, Rueckgabe:  false
    
  • Besserer Programm-Logger. Die Ausgabe dieses letzten Loggers war etwas schwer zu lesen, und es würde nur noch schlimmer werden, je mehr Sie ihn benutzten. Es wäre so viel einfacher zu lesen, wenn er die Zeilen in den inneren Blöcken einrücken würde. Um dies zu tun, müssen Sie verfolgen, wie tief verschachtelt Sie jedes Mal sind, wenn das Log aufgerufen wird. Verwenden Sie dazu eine globale Variable, das ist eine Variable, die Sie von überall in Ihrem Code sehen können. Um eine globale Variable zu erstellen, stellen Sie Ihrem Variablennamen einfach ein $ voran, wie diese: $global, $verschachtelungsTiefe und $grosserPeeWee. Am Ende sollte Ihr Logger Code wie diesen ausgeben:
    Beginne "aeusserer Block"...
      Beginne "ein kleiner Block"...
        Beginne "winzig kleiner Block"...
        ..."winzig kleiner Block" beendet, Rueckgabe:  viel Liebe
      ..."ein kleiner Block" beendet, Rueckgabe:  42
      Beginne "noch ein anderer Block"...
      ..."noch ein anderer Block" beendet, Rueckgabe:  Ich liebe indisches Essen!
    ..."aeusserer Block" beendet, Rueckgabe:  true
    

Nun, das war's für dieses Tutorial. Herzlichen Glückwunsch! Sie haben viel gelernt. Vielleicht haben Sie das Gefühl, dass Sie sich an nichts davon erinnern, oder vielleicht haben Sie einige Teile übersprungen... Entspannen Sie sich. Beim Programmieren geht es nicht darum, was man weiß; es geht darum, was man herausfinden kann. Solange Sie wissen, wo Sie die Dinge finden, die Sie vergessen haben, machen Sie sich gut. Ich hoffe, Sie denken nicht, dass ich das alles geschrieben habe, ohne jede Minute Dinge nachzuschlagen! Denn das habe ich getan. Ich habe auch viel Hilfe bei den Codebeispielen in diesem Tutorial bekommen. Aber wo habe ich alles nachgeschlagen und wen habe ich um Hilfe gebeten? Lassen Sie mich vorstellen...