Ruby-Programmierung: Funktionale Aspekte

Zurück zum Inhaltsverzeichnis.

In der Einleitung zu diesem Buch wurde erwähnt, dass Matz versuchte, mit Ruby Aspekte der objektorientierten und der funktionalen Programmierung zusammenzuführen. Diese Seite beschäftigt sich nun mit den funktionalen Anteil in Ruby.

Dazu eine kurze Einführung: Stellen Sie sich vor, Sie schreiben eine Methode zum Sortieren von Arrays. Dann implementieren Sie einen Algorithmus und abhängig davon, welches Element als kleiner bzw. größer angesehen werden soll, steht das Größte am Schluss ganz hinten. Jetzt stellen Sie sich vor, Sie wollen die umgekehrte Reihenfolge. Oder noch schlimmer: Sie wollen statt nach Anfangsbuchstaben von Strings nach deren Länge sortieren. Sie müssen ggf. viele Funktionen schreiben, die sich alle den Algorithmus teilen. Ruby ermöglicht es, an dieser Stelle die Auswahl der Reihenfolge an den Benutzer der Methode zu übergeben und nur den Algorithmus zu schreiben. Dies geschieht dadurch, dass die Sortiermethode vom Benutzer eine Methode als Parameter erhält, die aufgerufen wird, wenn es um diese Entscheidung geht.

Methodenaufrufe können zusätzlich zu Parametern noch einen sogenannten Block entgegennehmen. Ein sehr einfaches Beispiel hierfür ist Integer#times, wobei eine Zahl den Block entsprechend ihrem Wert x-mal aufruft. Optional können Blöcke Parameter aufnehmen, mehrere Parameter werden dann durch Kommata getrennt. Diese werden zwischen zwei Pipe-Zeichen geschrieben:

3.times { |i| puts "Hallo Nummer " + i.to_s }

Man kann auch folgende Syntax verwenden:

3.times do |i|
puts "Hallo" + i.to_s
end

Die beiden Varianten unterscheiden sich in ihrer Bindung zum davorgehenden Objekt. So führt ein 1.upto 3 { |i| puts i } zu einem Fehler, weil nicht die Methode upto den Block übergeben bekommt, sondern die Zahl 3, die dann mit dem Block nichts anfangen kann. Entweder schreiben man die 3 also in Klammern, oder verwendet do ... end.

Blöcke in eigenen Methoden

Bearbeiten

Für die Verwendung von Blöcken in eigenen Methoden können diese mit dem Schlüsselwort yield aufgerufen werden:

def execute
  yield
end

execute { puts "Hallo" }

Wenn diese Methode ohne Block aufgerufen wird, dann kommt es zu einem Fehler, da versucht wird nicht existenten Code auszuführen. Das kann man mit der Methode block_given? prüfen und in den Kontrollfluss der Methode einbauen, um eventuell eine Defaultmethode auszuführen, oder einen Fehler zurückzugeben

def execute
  yield if block_given?
end

Die Übergabe von Parametern an den Block erfolgt, wie bei normalen Methoden auch in einer mit Kommas getrennten Liste hinter yield.

Der & Operator

Bearbeiten

Es ist alternativ möglich den Block in einer lokalen Variable zu speichern, um ihn beispielsweise an andere Methoden weiter zu übergeben.

def execute(&block)
  block.()
end

Dabei wird eine Instanz der Klasse Proc erzeugt und in der Variable block gespeichert. Dadurch ist möglich auch mit normalem Kontrollfluss auf die Übergabe einen Blockes zu reagieren und die Variable direkt zu manipulieren. Es ist nur Möglich einen Block an eine Methode zu übergeben. Wichtig bei der Übergabe an eine andere Methode ist das erneute Auspacken des Blockes aus der Objektinstanz wieder mit dem & Operator:

def five_times(&block)
  5.times(&block)
end

Iteratoren

Bearbeiten

Die wichtigsten Methoden in diesem Zusammenhang sind Iteratoren. Sie arbeiten auf Objekten, die mehrere Objekte beinhalten, zum Beispiel Arrays oder Hashes und deren Aufgabe ist es jedes Element an einen übergebenen Codeblock zu übergeben. Das folgende Skript implementiert eine eigene Version von Array#each, die ähnliche Funktionalität bereitstellt.

class Array
  def my_each(&block)
    if block
      for i in (0..self.length)
        block.(self[i])
      end
    else
      raise ArgumentError
    end
  end
end

[1, 2, 3].my_each { |i| puts i }

Objektinstanzen von ausführbarem Code

Bearbeiten

Wie schon durch die Verwendung von & vorausgenommen, ist es möglich, Objektinstanzen von ausführbarem Code anzulegen und diese in Variablen zu speichern, zu manipulieren und an andere Methoden zu übergeben. In Ruby gibt es dafür sehr viele verschiedene Möglichkeiten, die hier vorgestellt werden sollen.

procs = []
procs << -> x { x + 1 }
procs << lambda { |x| x + 1 }
procs << Proc.new { |x| x + 1 }
procs << proc { |x| x + 1 }

procs.each do |p|
  begin
    puts p.class
    puts p.(1, 2)
  rescue
    puts "Falsche Parameter"
  end
end

Der Unterscheid zwischen den Methoden zeigt sich auch im obigen Skript. Er besteht beim Aufruf der Objekte mit einer falschen Anzahl an Parametern. Lambdaausdrücke und die in Ruby 1.9 eingeführte -> Notation verhalten sich eher wie Methoden, während die Procmethoden nicht ihre Parameter überprüfen. Dadurch kann es zu seltsamen Fehlern bei der Verwendung von zu wenigen Parametern kommen, die schwer zu finden sind. Die ersten beiden gezeigten Möglichkeiten sind daher zu bevorzugen, da der Fehler näher am falschen Code auch entdeckt wird.