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.
Blöcke
BearbeitenMethodenaufrufe 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
Bearbeitenyield
BearbeitenFü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
BearbeitenEs 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
BearbeitenDie 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
BearbeitenWie 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.