Tief im Osten

Scriptsprachen sind nicht nur beliebt, es gibt sie auch wie den berühmten Sand am Meer. Um mit einer neuen Erfolg haben zu können, muss eine Sprache schon Besonderes aufweisen. Objektorientierung ist da das Mindeste.

vorlesen Druckansicht 56 Kommentare lesen
Lesezeit: 12 Min.
Von
  • Clemens Wyss
Inhaltsverzeichnis

Man mische die pure Objektorientiertheit von Smalltalk mit der konzisen Syntax von Eiffel. Das Ganze gut rühren und einen Esslöffel von Perls Komfort und Mächtigkeit beifügen. Zu guter Letzt noch mit jeweils einer Prise von Python-, Scheme-, Sather- und CommonLisp-Vorzügen würzen. Fertig ist Ruby.

Generell gewinnen Scriptingsprachen mehr und mehr an Akzeptanz, nicht nur bei Systemadministratoren und CGI-Entwicklern. Vielmehr werden die so genannten Very High Level Languages (VHLL) vermehrt auch für Prototyp-, Test- und ‘Legacy System Integration’-Projekte herbeigezogen. Der Grund hierfür liegt auf der Hand: VHLL bieten einen hohen Abstraktionsgrad und erlauben deshalb schnelles Entwickeln (Rapid Application Development, RAD). Yukihiro Matsumoto (mit Spitznamen ‘Matz’) sagt über sein Kind: ‘In Ruby sind einfache Dinge einfach zu lösen und komplexe Dinge machbar’. 1995 erblickte Ruby in der Version 0.95 das Licht der (japanischen) Öffentlichkeit.

Mehr Infos

Zudem bieten sämtliche VHLL (Ruby, Perl, Python und so weiter) die Möglichkeit, zeitkritische Teile in C/C++ auszulagern oder bestehende DLLs beziehungsweise .so-Bibliotheken einzubinden. Dieser Artikel soll die speziellen Vorzüge von Ruby (gegenüber Perl, Python, Java und C++) aufzeigen. Unter www.ruby.ch steht ein Online-Interpreter zur Verfügung, mit dem die Codebeispiele nachvollzogen werden können. Er gewährt einen ersten Einblick in die Scriptingsprache ohne Installation des Interpreters auf dem eigenen Rechner. Zudem ist das gesamte Buch von Thomas und Hunt [1] online verfügbar, sodass Interessierte sämtliche Codebeispiele ausprobieren können.

In Japan hat Ruby bereits Fuß gefasst (Rang zwei hinter Perl, aber bereits vor Python). Die westliche Welt zieht nach, denn nur so lässt sich das hohe Aufkommen auf der comp.lang.ruby-Newsgroup in den letzten drei Monaten erklären. Meines Erachtens absolut verständlich, betrachtet man die konzise und pure objektorientierte Syntax der Sprache.

Ruby ist eine rein objektorientierte Scriptsprache. Alles Erdenkliche ist ein Objekt. Im Unterschied zu Java, C++ oder Python wird nicht zwischen Basistypen (Nummern und Zeichenketten) und normalen Klassen unterschieden. Funktionsaufrufe sind stets Meldungen, die ein Objekt geschickt bekommt. Dabei steht der Empfänger links (in diesem Fall der String ‘Guten Tag’), die Funktion rechts vom Punkt.

"Guten Tag".length()
-> 9
-12.abs()
-> 12
auto.hupe()
-> "huup"

Typen kennt Ruby nicht. Auch entfällt die Deklaration von Variablen. Vielmehr ‘entstehen’ Variablen durch die erstmalige Zuweisung eines Werts auf sie.

gruss = "hallo Welt"
grosseNummer = 4333332323

Will man eine Variable global verfügbar machen, muss der Variablenname mit einem ‘$’ beginnen.

$globalVariable = "Ich bin global sichtbar"
lokalVariable = "Ich bin nur lokal sichtbar"

Ruby unterscheidet nicht zwischen Prozeduren und Funktionen, es gibt nur Funktionen (wie bei Lisp). Der Wert einer Funktion ist entweder der Rückgabewert einer return-Instruktion oder der Wert des letzten Ausdrucks (Expression) in der Funktion. Die Deklaration einer Funktion erfolgt durch die Anweisung def gefolgt vom Funktionsnamen und einer Argumentliste in Klammern:

def sagsMehrmals(wort = "nichts", anzahl = 1)
...
end

Man beachte, wie in der Deklaration von sagsMehrmals Standardwerte für die Argumente gesetzt werden können ("nichts" für wort und 1 für anzahl). Diese kommen zum Tragen, wenn beim Aufruf der Funktion die entsprechenden Argumente fehlen (siehe Default-Values in C++). Neben der statischen Argumentliste (ihre Anzahl ist bekannt) kann eine Funktion auch eine variable Anzahl von Argumenten verarbeiten (dynamische Liste).

def sagDieWorte(*worte)
i = 0
while (i < worte.length()) do
print (worte[i])
i += 1
end
end
sagDieWorte("hallo", "Welt") -> halloWelt
sagDieWorte("eins", "zwei", "drei") -> einszweidrei

Eine Klassendeklaration beginnt mit class, gefolgt vom Klassennamen. Instanzen erzeugt man durch den Aufruf von Klassenname.new(). Klassen besitzen Instanz- und Klassenfunktionen (static member functions). Zur Unterscheidung dieser beiden Konzepte wird bei der Deklaration einer Klassenfunktion der Name dem Funktionsnamen vorangestellt.

class Auto
def fahre() # Instanzfunktion
...
end
def Auto.alleMarken() # Klassenfunktion
...
end
end
a = Auto.new()
a.fahre()
Auto.alleMarken()

Von Java und C++ entlehnt sind die Schlüsselwörter public, protected und private. Diese dienen in Ruby als Sichtbarkeitsbegrenzer von Funktionen. Auch in Ruby gilt (falls nicht explizit angegeben) die Sichtbarkeitsregel public.

class Auto
public
def f1
...
end
protected
def f2
...
end
private
def f3
...
end
end

Zur Unterscheidung von Klassen- und Instanzvariablen wurde das at-Zeichen (‘@’) herbeigezogen. Eine Deklaration von Klassen- und Instanzvariablen entfällt (wegen der Typenlosigkeit). Vielmehr werden auch diese bei der erstmaligen Zuweisung ins Leben gerufen. Instanzvariablen sind mit einem ‘@’, Klassenvariablen mit zweien gekennzeichnet.

class Auto
def initialize(name)
@name = name # Instanzvariable
@@anzahlAutos += 1 # Klassenvariable
end
end

Auf sämtliche Klassen- und Instanzvariablen gibt es keinen direkten Zugriff (C++: private members). Als kleines Zückerchen kann man in Ruby mit den Funktionen attr_reader und attr_writer die so genannten Navigations-Funktionen (auch getter und setter genannt) einfach erzeugen.

class Auto
attr_reader (:motor, :getriebe)
attr_writer (:motor, :getriebe)
end
a = Auto.new()
a.motor = "TDI"
a.motor()
-> "TDI"
a.getriebe= "Tiptronic"
a.getriebe()
-> "Tiptronic"

Obige Deklarationen erzeugen implizit die getter-Funktionen motor()/getriebe() sowie die setter-Funktionen motor=(obj)/getriebe=(obj) - außerdem wird beim erstmaligen Aufruf von motor=(obj)/getriebe=(obj) die Instanzvariable @motor beziehungsweise @getriebe initialisiert. Noch kürzer geht’s mit dem Schlüsselwort attr_accessor, das sowohl einen attr_reader als auch einen attr_writer erzeugt. Die Funktionen attr_reader(), attr_writer() und attr_accessor() sind in der Klasse Module implementiert. Es handelt sich hierbei also nicht um Deklaratoren (wie def, class oder module), sondern um Funktionsaufrufe, die der Interpreter beim Laden des Scripts vornimmt.

Als ‘Konstruktor’ dient die Funktion initialize(), die der Ruby-Interpreter aufruft, nachdem der Speicher für das Objekt alloziert ist. Dabei fließen die Argumente des new() Aufrufs in die initialize-Funktion.

class Auto
def initialize(name, motor)
@name = name
@motor = motor
end
...
end
a = Auto.new("Volvo V70", "T5")

Vererbung kennzeichnet das Symbol ‘<’, gefolgt von der Basisklasse (der zu ‘beerbenden’).

class Auto < Fahrzeug
def initialize(name)
super(name)
...
end
end

Fehlt eine explizite Vererbungsdeklaration, ist implizit immer die Klasse Object die Basisklasse. Der Einfachheit halber verzichtet Ruby auf Mehrfachvererbung. Der Aufruf von super() in einer Funktion ruft dieselbe in der Basisklasse auf, in diesem Falle die initialize()-Funktion von Fahrzeug. Die eigentlich nicht vorgesehene Mehrfachvererbung ist durch ein ‘Mix-In’ von Modulen zu erzielen.

Ruby besitzt einen ‘Mark-and-sweep’ Garbage Collector. Der beseitigt automatisch jedes nicht mehr von einem anderen referenzierte Objekt. Damit entfällt die Notwendigkeit von Destruktoren. Außerdem ist die Entscheidung überflüssig, zu welchem Zeitpunkt ein Objekt gelöscht (delete in C++, free in C) werden muss/darf.

Der ‘Mark-and-sweep’-Algorithmus hat vor allem dann angenehme Konsequenzen, wenn man in C/C++ eine Erweiterung zu Ruby schreiben möchte. Hierbei muss sich der Erweiterungs-Entwickler nicht mit den leidigen Referenzzählern (wie in Perl und Python) herumschlagen.

Kontrollstrukturen bilden Blöcke. Ein Block ist eine Code-Sequenz zwischen geschweiften Klammern ({...}) oder den Schlüsselwörtern do und end. Solche Blöcke treten nur im Zusammenspiel mit Funktionsaufrufen auf, wie Listing 1 sie zeigt.

Mehr Infos

Listing 1: Blöcke

funktionsAufruf(<Argumente>)[ { | <Block-Argumente> | <Block-Listing> }]

funktionsAufruf(<Argumente>)[ do | <Block-Argumente> | <Block-Listing> end ]

Trifft Ruby auf einen Block, wird dieser nicht sofort ausgeführt, sondern vorerst auf die Seite gelegt. In Aktion tritt er erst, wenn die aufgerufene Funktion ein yield() enthält.

def dupliziereMitNachbehandlung(zahl)
result *= 2
if block_given? then
yield (result)
else
result
end
end
dupliziereMitNachbehandlung(3)
-> 6
dupliziereMitNachbehandlung(3) { | wert | wert + 1}
-> 7

Dabei ist viererlei zu beachten:

  • Die Deklaration der Funktion enthält keinen Block.
  • yield gibt die Kontrolle an den Block ab (mit beliebig vielen Argumenten).
  • block_given? prüft, ob dem Funktionsaufruf ein Block angehängt wurde.
  • Jeder Funktion kann ein Block ‘mitgegeben’ werden.

Die Blöcke spielen eine wichtige Rolle bei Iterationen. Aus der Sicht des ‘Visitor Pattern’ - das mit dem ‘Iterator Pattern’ einhergeht - ist der Block der Besucher (Visitor).

class Menge
...
def fuerJedesElement()
while i < @elemente.length() do
yield(@elemente[i])
end
end
...
end
m = Menge.new()

m.fuerJedesElement() { | element | ...}

Die Mächtigkeit des Visitor Pattern besteht darin, dass ein beliebiger Block (einzig die Anzahl Argumente muss stimmen) der fuerJedesElement()-Funktion mitgegeben werden kann. Rubys Aufzählklassen (Array[] und Hash{}) implementieren die Funktion each(), die über die Elemente iterieren kann.

Module dienen als Namensraum- und Modularisierungskonstrukt. Die Klasse Module ist zudem die Basisklasse von Class. Im Unterschied zu Klassen lassen sich Module nicht instanziieren, aber in Klassen ‘einmischen’ (Mix-in). Dadurch übernehmen die Instanzen der Klasse die im Modul deklarierten Funktionen.

module Serialisierbar
def speichereIn(datei)
...
end
def leseVon(datei)
...
end
end
class Auto
include Serialisierbar
end
a = Auto.new()
a.leseVon("auto.txt")
a.speichereIn("auto.txt")

Im Gegensatz zu Javas Interfaces enthalten Module nicht nur die Schnittstellendeklaration, sondern auch deren Implementierung. Dies ergibt gewissermaßen die eigentlich nicht vorhandene Mehrfachvererbung.

Ruby bietet mächtige Reflexionsmechanismen. Diese mögen zwar nicht für den täglichen Gebrauch gedacht sein, geben dem Entwickler jedoch Werkzeuge in die Hand, die er in ‘Sonderfällen’ nicht missen möchte. Anbei seien nur einige Stilblüten aufgeführt, wie sie Listing 2 zeigt.

Mehr Infos

Listing 2

1) 

eval("class Hallo; def sagHallo;
print 'hallo'; end;
end; h = Hallo.new(); h.sagHallo()")
class Auto
def initialize(marke, motor)
@marke = marke; @motor = motor
end
def fahreNach(ort)
print ("ich fahre nach #{ort}\n")
end
def to_s
"#{@marke} mit #{@motor}-Motor\n"
end
end
auto = Auto.new("Porsche","Boxer")

2)

p auto.instance_variables

3)

p Auto.public_instance_methods

4)

class << auto
def neueFunktion()
print ("es funktioniert\n")
end
end
auto.neueFunktion()

5)

autoFahren = auto.method("fahreNach")
autoFahren.call("München")

6)

ObjectSpace.each_object(Auto) { | auto | print
( auto.to_s + " wird noch referenziert ")}

Im ersten Beispiel erlaubt eval() die Ausführung eines beliebigen Code-Strings. Das zweite zeigt, welche Instanzvariablen ein Objekt besitzt. Das dritte Code-Schnipsel gibt aus, welche (public) Instanzfunktionen eine Klasse enthält. Im vierten Beispiel wird einer Instanz um eine Funktion (eine so genannte Singleton-Methode) erweitert. Dabei wird nicht die Klasse der Instanz erweitert, sondern nur die Instanz ‘per se’. Der fünfte Teil demonstriert First-class-Objekte, und der sechste zählt Instanzen einer Klasse (siehe auch den Kasten zum Sandbox-Beispiel oben).

Interessanterweise hat Matsumoto Ruby einige Patterns in die Wiege gelegt. Im Speziellen das Delegator-, Visitor- (wie gesehen), Singleton- und vor allem das Observer-Pattern. Zu den angenehmen Eigenschaften der Sprache gehört, dass Objekte sich einfach serialisieren lassen. Außerdem existiert eine COM-Anbindung. Und Ruby ist nicht zuletzt wegen des ‘Mark-and-sweep’-GC-Algorithmus leicht durch C/C++ Module erweiterbar. Schließlich implementiert die neue Sprache native Threads und Thread-Gruppen und verfügt über eine ausgeklügelte Ausnahmebehandlung (Exception Handling).

Möglicherweise gehört die Zukunft solchen Hochsprachen. Zumindest in der .NET-Welt scheinen sich die Grenzen zwischen Compiler- und Interpreter-Sprachen zu verwischen: C#, Eiffel#, Perl# - vielleicht demnächst sogar Ruby# ...

Natürlich kann dieser Artikel nicht sämtliche Stärken (Schwächen gibt’s kaum) von Ruby aufzeigen. Zum Einstieg empfehlenswert ist das bei Addison-Wesley erschienene Buch ‘Programming Ruby’ von Thomas und Hunt.

Clemens Wyss
ist Projektleiter und Berater bei der Helbling Technik AG in Aarau (Schweiz).

[1] David Thomas, Andrew Hunt; Programming Ruby: A Pragmatic Programmer’s Guide, Reading, MA (Addison-Wesley) 2001

[2] Dave Thomas, Andy Hunt; Programming in Ruby; A freely available pure object-oriented language; Dr. Dobb’s Journal 1/2001; http://www.ddj.com/articles/2001/0101/0101b/0101b.htm

Mehr Infos

iX-TRACT

  • Scriptsprachen erfreuen sich immer noch wachsender Beliebtheit; aus Japan kommt mit Ruby eine, die rein objektorientiert ist.
  • Ruby ist eine Mischung aus der Objektorientierung von Smalltalk und der Flexibilität von Perl.
  • In Japan ist Ruby nach Perl die meistverwendete Scriptsprache.