Lektion 19 - Containers, Blocks und Iterators 2

Erstellt von Frithjof Sat, 01 Mar 2008 07:29:00 GMT

Eine der Stärken von Ruby sind die leicht zu benutzenden Datenstrukturen, die als Behälter für alle möglichen Daten dienen. Du hast von diesen Container genannten Strukturen bereits Listen und Hashes kennengelernt.

In dieser Lektion lernst du die beiden anderen Talente von Ruby kennen: Blocks (engl. für Blöcke) und Iterators (engl. für Iteratoren, aus dem lateinischen iter, das Gehen, der Weg oder der Marsch).

Dann, mit der nächsten Lektion 20, geht dieser Teil von rubykids.de schließlich zu Ende und es beginnt mit Lektion 21 ein neuer. Mal sehen, was wir da spannendes machen werden.

Blocks und Iterators

Die Geschichte von Blöcken und Iteratoren ist schnell erzählt. Der Iterator (ein alter Römer) greift sich während seines beschwerlichen Marsches entlang eines bis zum Rand mit Eisen gefüllten Containers nacheinander einen Klumpen des harten Elements, erhitzt ihn, legt ihn auf einen Block und haut solange ordentlich drauf, bis ein geschmeidiger Stahl den Amboss wieder verlässt.

Einige hundert Jahre später können wir das mit Ruby etwa wie in folgendem Beispiel formulieren:


eisenklumpen_container = ["klumpen1", "klumpen2", "klumpen3", "klumpen4"]

eisenklumpen_container.each do |klumpen|
  puts "Erhitze den #{klumpen}..." 
  puts "Schlage den #{klumpen} auf dem Amboss..." 
  puts "... zu geschmeidigem Stahl." 
  puts
end

Die Liste eisenklumpen_container ist ein Objekt der Container-Klasse Array. Der alte römische Iterator ist hier each, eine Methode der Klasse Array. Der Iterator each ruft für jedes Element klumpen im eisenklumpen_container den vier-zeiligen Codeblock auf, der sich zwischen do und end befindet. Im Verlauf macht der Iterator das Element, das er gerade verarbeitet dem Codeblock über die Variable klumpen bekannt.

Bisher hatten wir für die aufeinanderfolgende Bearbeitung von Elementen aus einem Container mit der for Schleife hantiert. Das obige Beispiel würde sich dann so darstellen:


eisenklumpen_container = ["klumpen1", "klumpen2", "klumpen3", "klumpen4"]

for klumpen in eisenklumpen_container do 
  puts "Erhitze den #{klumpen}..." 
  puts "Schlage den #{klumpen} auf dem Amboss..." 
  puts "... zu geschmeidigem Stahl." 
  puts
end

Hier gibt es nur den Container und den Codeblock, aber keinen Iterator. Zumindest sieht man ihn nicht. Der Rubyinterpreter ersetzt aber hinter den Kulissen die for-Schleife durch einen each-Iterator. Beides ist somit gleichwertig verwendbar.

Der each-Iterator ist schon ganz prima, nur ist er nicht überall sinnvoll einsetzbar. Stellen wir uns einen Hash vor, den aus der Theorie Lektion 5. Was sollte bei einem Hash ein each Iterator tun? Soll er alle Schlüssel des Hash ablaufen, oder alle Werte? Oder beides zugleich?


deutsch_spanisch = {
  "eins"   => "uno",
  "zwei"   => "dos",
  "drei"   => "tres",
  "vier"   => "cuatro",
  "fuenf"  => "cinco", 
  "sechs"  => "seis",
  "sieben" => "siete",
  "acht"   => "ocho",
  "neun"   => "nueve",
  "zehn"   => "diez",
}

deutsch_spanisch.each_key do |key|
  puts "Iterator bearbeitet gerade den Schluessel: #{key}" 
end

deutsch_spanisch.each_value do |value|
  puts "Iterator bearbeitet gerade den Wert: #{value}" 
end

deutsch_spanisch.each_pair do |key, value|
  puts "Iterator bearbeitet gerade das Paar: #{key}, #{value}" 
end

Ein Hash hat also sogar drei verschiedene Iteratoren! Der Iterator each_key durchläuft alle Schlüssel des Hash nacheinander, während der Iterator each_value sich die Werte vornimmt. Der each_pair Iterator schnappt sich sowohl den Schlüssel, als auch den zugehörigen Wert aus dem Hash und wirft ihn dem Code-Block zur Verarbeitung vor.

Dann wollen wir doch mal sehen, ob das Erfinden eines Iterators auch so einfach ist, wie seine Verwendung.

Einen Iterator bauen

In diesem Abschnitt erstellen wir einen eigenen Iterator. Er soll each_mit_nachfolger heißen und wie sein kleiner Bruder each auch alle Elemente einer Liste abschreiten, dabei aber gleichzeitig immer auch das nachfolgende Element mit anfassen. Gewünscht sei folgende Verwendungsmöglichkeit für den Iterator.


liste = MeineListe.new(["eins", "zwei", "drei", "vier"])

liste.each_mit_nachfolger do |element, nachfolger|
  puts "'#{element}' kommt in der Liste vor '#{nachfolger}'" 
end

Wir legen dafür eine Klasse MeineListe an und spendieren ihr eine einzige Methode each_mit_nachfolger als Iterator. MeineListe soll sich ansonsten genauso wie ein gewöhnliches Array verhalten, das bedeutet sie erbt von der Klasse Array. Oder wir sagen dazu auch wir leiten die Klasse MeineListe von der Klasse Array ab.


class MeineListe < Array
  def each_mit_nachfolger
  end
end

Bisher also nichts besonderes. Aber die Methode each_mit_nachfolger ist noch lange kein Iterator. Sie macht bisher ja noch überhaupt nichts.

Lassen wir sie zuerst dasselbe machen wie der Iterator each.


class MeineListe < Array
  def each_mit_nachfolger
    each do |element|
    end
  end
end

Der Aufruf von each innerhalb von each_mit_nachfolger funktioniert deswegen, weil each eine Methode der Klasse Array ist und durch die Vererbung ist diese Methode auch auf die Klasse MeineListe übergegangen.

Das Element element müssen wir nun an den Block weitergeben, der vielleicht beim Aufruf der Methode each_mit_nachfolger mitgegeben wurde. Für dieses Aufrufen eines Code-Blocks und das Weitergeben von Werten an diesen Block hat Ruby ein spezielles Schlüsselwort parat: es heißt yield.


class MeineListe < Array
  def each_mit_nachfolger
    each do |element|
      yield(element) if block_given?
    end
  end
end

yield steht stellvertretend für den unsichtbaren Code-Block. Und wie bei einem gewöhnlichen Methodenaufruf können wir yield auch als Parameter die Werte mitgeben, die dann im Block zur Verarbeitung verfügbar sein sollen. Falls kein Block beim Aufruf übergeben wird, darf yield nicht aufgerufen werden. Daher der Schutz von yield mit der if-Abfrage. Andernfalls würde der Aufruf des Iterators ohne Code-Block zu einem Fehler führen.


liste.each_mit_nachfolger

C:\entwicklung>ruby lektion_19.rb
lektion_19.rb:56:in `each_mit_nachfolger': no block given (LocalJumpError)
        from lektion_19.rb:52:in `each'
        from lektion_19.rb:52:in `each_mit_nachfolger'
        from lektion_19.rb:68

Die Methode block_given? wird von der Klasse Object bereitgestellt (über das Modul Kernel, Module schauen wir uns später noch an) und ist in jeder anderen Klasse automatisch verfügbar, denn am oberen Ende jeder Vererbungshierarchie steht die Klasse Object. Das trifft auch für unsere selbst definierte Klasse MeineListe zu, wie folgende irb Sitzung schnell beweist:


C:\entwicklung>irb
irb(main):001:0> require 'lektion_19'
=> true
irb(main):002:0> MeineListe.superclass
=> Array
irb(main):003:0> Array.superclass
=> Object
irb(main):004:0>

Nun gut, dann probieren wir unseren selbst gebastelten Iterator einmal aus.


class MeineListe < Array
  def each_mit_nachfolger
    each do |element|
      yield(element) if block_given?
    end
  end
end

liste = MeineListe.new(["eins", "zwei", "drei", "vier"])

liste.each_mit_nachfolger do |element, nachfolger|
  puts "'#{element}' kommt in der Liste vor '#{nachfolger}'" 
end

C:\entwicklung>ruby lektion_19.rb
'eins' kommt in der Liste vor ''
'zwei' kommt in der Liste vor ''
'drei' kommt in der Liste vor ''
'vier' kommt in der Liste vor ''

Prima, alle Element der Liste werden abgearbeitet. Aber das konnte der each Iterator auch schon. Wir passen unseren eigenen Iterator jetzt so an, dass er wirklich neben dem aktuellen Element in der Liste den jeweils aktuellen Nachfolger gleich mit bearbeitet.


class MeineListe < Array
  def each_mit_nachfolger
    akt_nachfolger = nil
    akt_element    = nil
    each do |element|
      akt_element    = akt_nachfolger
      akt_nachfolger = element
      if akt_element != nil
        yield(akt_element, akt_nachfolger) if block_given?
      end
    end
  end
end

liste = MeineListe.new(["eins", "zwei", "drei", "vier"])

liste.each_mit_nachfolger do |element, nachfolger|
  puts "'#{element}' kommt in der Liste vor '#{nachfolger}'" 
end

Und dann sieht die Ausgabe wie erwartet aus:


C:\entwicklung>ruby lektion_19.rb
'eins' kommt in der Liste vor 'zwei'
'zwei' kommt in der Liste vor 'drei'
'drei' kommt in der Liste vor 'vier'

Der Iterator hält rechtzeitig bei der drei mit Nachfolger vier an, weil nach der vier die Liste ja zu Ende ist.

Blöcke als Konservendose

Ein Iterator übergibt alle Elemente, die er abläuft an einen Code-Block. Die Zeilen des Code-Blocks stehen dabei zwischen dem do und dem end. Das muss aber nicht so sein.

Wie der Name Block schon sagt, kann ein Block auch alleine für sich stehen.


element    = nil
nachfolger = nil
ein_block = Proc.new {  puts "'#{element}' kommt in der Liste vor '#{nachfolger}'" }
ein_block.call

C:\entwicklung>ruby lektion_19.rb
'' kommt in der Liste vor ''

Man kann Rubycodezeilen in einer Variablen speichern! Alle Codezeilen werden in einem Objekt der Klasse Proc verwaltet. Dieses Objekt ist es dann, was in der Variablen festgehalten wird. Über die Methode call der Klasse Proc lässt sich der Codeblock dann leicht aufrufen. In dem kleinen Beispiel tut sich aber nichts, weil noch niemand die Variablen element und nachfolger sinnvoll belegt hat. Ein Codeblock macht somit nur Sinn, wenn er in einer passenden Umgebung verwendet wird, wie zum Beispiel beim Aufruf eines Iterators:


element    = nil
nachfolger = nil
ein_block = Proc.new {  puts "'#{element}' kommt in der Liste vor '#{nachfolger}'" }
liste.each_mit_nachfolger do |element, nachfolger|
  ein_block.call
end

puts element
puts nachfolger
ein_block.call

Code-Blöcke haben eine weitere interessante Eigenschaft, die ebenfalls zu den wichtigen Kernkompetenzen von Ruby gehört. Ein Codeblock merkt sich den Kontext, in dem er erzeugt wurde. Mit Kontext sind hier insbesondere alle Variablen gemeint, die bei der Anlage des Codeblocks erreichbar sind. Dabei merkt sich der Codeblock nicht den aktuellen Wert der Variable, sondern die Variable selbst. Und damit kann der Codeblock jedesmal, wenn er aufgerufen wird den dann zum Aufrufzeitpunkt aktuellen Variablenwert verwenden.

Im letzten Beispiel oben haben wir am Ende des Aufrufs vom Iterator each_mit_nachfolger nochmal die Werte der beiden Variablen element und nachfolger ausgegeben. Sie waren jetzt am Ende des Interatordurchlaufs natürlich mit den beiden letzten Werten belegt, die vom Iterator angefasst wurden, also drei und vier. Rufen wir danach außerhalb des Iterators nochmal den Codeblock auf, so verwendet er ja dieselben Variablen, die bei seiner Erzeugung schon bekannt waren, also element und nachfolger, natürlich aber mit ihrem jeweiligen aktuellen Werten.

Vielleicht hilft noch ein weiteres kleines Beispiel, um dieses auf den ersten Blick merkwürdige Verhalten zu verstehen. Betrachten wir folgenden Code:


sag_hallo = nil

1.times do
  name = "Peter" 
  sag_hallo = Proc.new { puts "Hallo #{name}!" }
  name = "Eulalia" 
end

name = "Livia" 

sag_hallo.call

Wir definieren eine Variable sag_hallo für einen Codeblock. Dann rufen wir den Iterator times der Klasse Fixnum für die Zahl 1 auf. Innerhalb des Iteratoraufrufs definieren wir eine lokale Variable name und setzen sie auf den Wert Peter. Anschließend erst erzeugen wir den Codeblock selbst, auch innerhalb des Iteratoraufrufs times. Danach setzen wir die Variable name auf einen anderen Wert und verlassen den Iterator.

Wieder aus dem Iterator draußen setzen wir die Variable name auf den Wert Livia und rufen anschließend den Codeblock sag_hallo auf. Was wird er ausgeben? Es gibt drei Möglichkeiten:

  1. Hallo Peter!
  2. Hallo Eulalia!
  3. Hallo Livia!

Probieren wir es einfach aus:


C:\entwicklung>ruby lektion_19.rb
Hallo Eulalia!

Die Begründung ist wieder der Kontext bei der Erzeugung des Codeblocks. Zum Zeitpunkt, als der Codeblock sag_hallo angelegt wird, ist eine Variable name bekannt, die aber nur lokal innerhalb des Iteratoraufrufs times sichtbar ist.

Die gleichnamige Variable name außerhalb des Iteratoraufrufs ist eine andere Variable!

Es gibt hier also zwei verschiedene Variablen mit dem gleichen Namen! Zum Zeitpunkt der Erzeugung des Codeblocks aber war nur die erste der beiden bekannt. Daher verwendet der Codeblock bei seinem Aufruf auch den letzten aktuellen Wert dieser Variablen.

Zur Kontrolle dieser Beobachtung definieren wir die Variable name einmal vor dem Iteratoraufruf:


sag_hallo = nil
name      = nil

1.times do
  name = "Peter" 
  sag_hallo = Proc.new {puts "Hallo #{name}!"}
  name = "Eulalia" 
end

name = "Livia" 

sag_hallo.call

Jetzt gibt es im gesamten Programm nur eine einzige Variable name und somit verwendet der Codeblock bei seinem letzten Aufruf auch den dann aktuellen Wert dieser Variable:


C:\entwicklung>ruby lektion_19.rb
Hallo Livia!

Man nennt einen Codeblock in diesem Zusammenhang auch gerne eine Hülle, oder englisch Closure. Er hüllt alles in sich hinein, was zum Zeitpunkt seiner Entstehung in der Umgebung bekannt ist. Er legt seinen einhüllenden Mantel um alle Variablen, die bei seiner Erschaffung sichtbar sind. Alles was erst später zu Tage tritt, kennt er nicht.

Meine Nachricht

Einen Kommentar hinterlassen

  1. Niki about 1 month later:
    I have wanted one of these forever! THANKS for the great work
  2. salai 4 months later:
    ich wünche mir auch rekursion( mathe) aufgabe in Ruby kids site. grüße salai.