Ruby ist eine objektorientierte Skriptsprache, die sich einen Platz zwischen den etablierten Sprachen Perl, Python und vielleicht auch PHP erobert. Ruby, eine Entwicklung aus Japan, hat auch einen gewissen Unterhaltungswert, was den Einstieg erleichtert.
Aus Japan, dem Land der aufgehenden Sonne, kommt ein neuer Stern am Himmel der Skriptsprachen. Aber braucht die Welt eine neue Skriptsprache? Gibt es doch bereits etablierte wie Perl, Python, Tcl und viele mehr. Ruby ist ein neuer Ansatz für eine durchdachte und objektorientierte Skriptsprache, die leicht zu benutzen ist. Da sie recht jung ist, sind noch nicht so viele Libraries wie für Perl oder Python vorhanden, dennoch ist sie bereits für viele Zwecke voll einsetzbar. Dieser Artikel führt an einigen Beispielen die Konzepte vor und zeigt, wie kompakt, aber dennoch sauber strukturiert Ruby sein kann.
Eierlegende Wollmilch-Sprache?
Ruby wurde 1993 bis 1995 von Yukihiro (Matz) Matsumoto in Japan entwickelt; Perl 4 und Python gab es bereits. Während Python eine Hybridsprache mit Funktionen für prozedurale Programmierung und mit Objekten für die OO-Programmierung ist, ist Ruby rein objektorientiert: Es gibt keine Funktionen, nur Methoden – und alles ist ein Objekt. Dabei gibt es einen Ersatz für Funktionen: Eine außerhalb einer Klasse definierte Methode wird zu einer privaten Methode der Überklasse Object, wodurch sie überall verfügbar wird. Die Syntax und die Designphilosophie sind stark an Perl angelehnt, so gibt es Statement Modifiers ( if, unless, while und weitere ), eingebaute reguläre Ausdrücke, Sondervariablen wie $_ sowie den Unterschied zwischen “…” und ‘…’-Strings. Ruby bedient sich also bei diversen Programmiersprachen und vereinigt sie in einer neuen. Im Gegensatz zu Perl bezeichnen $, @ und % nicht unterschiedliche Datentypen, sondern den Scope einer Variablen: Normale Variablen kommen völlig ohne Zeichen wie $, % oder @ aus, $var bezeichnet eine globale Variable beliebigen Typs und @var eine Instanzvariable beliebigen Typs in einem Objekt. Semikola am Zeichenende gibt es in Ruby-Programmen nicht.
Objektorientiert
Wie bei allen OO-Sprachen auch stehen in Ruby praktische Standardklassen zur Verfügung, die sich leicht einsetzen lassen. Natürlich kann man auch selbst Klassen definieren, inklusive Vererbung sowie privater, geschützter und öffentlicher Methoden. Die Objektorientierung von Ruby geht allerdings noch ein ganzes Stück weiter, so ist kurz gesagt eigentlich alles, was sich modifizieren lässt, eine Instanz einer Klasse (beziehungsweise eine Referenz darauf). Das gilt auch schon für ganz normale Zahlen, so ist beispielsweise -42 eine Instanz der Integerklasse und verfügt somit auch über die in der Integerklasse definierten Methoden. Durch diese bis zu den Basiselementen reichende Objektorientierung erhält man Informationen über eine Zahl oder einen String nicht durch eine Funktion wie etwa sizeof(scalar) oder int2str(int), sondern direkt vom Objekt, eben durch eine seiner Methoden. Um beispielsweise die String-Repräsentation von 42 zu erhalten, kann man die Methode 42.to_s aufrufen. Das ist natürlich ein banales Beispiel, jedoch ist die (überschreibbare) Methode .to_s bereits in der Klasse Object definiert, der Mutter aller Klassen, und somit in absolut jeder Klasse – auch den eigenen – verfügbar. Neben den üblichen Klassen- und Instanzmethoden bietet Ruby die Möglichkeit, so genannte Iteratoren zu nutzen. Das sind Methoden, die über die einzelnen Elemente eines Objektcontainers iterieren, also über die Elemente eines Arrays, eines Hashes, die Zeilen einer Textdatei oder auch einer eigenen Containerklasse. Sogar die Integerklasse, selbst eigentlich kein Container, verfügt über hilfreiche Iteratoren wie etwa .upto(num) oder .downto(num), die ausgehend vom Objekt in Einerschritten bis num iterieren. Dabei übergibt man dem Iterator einen Funktionsblock, der der Reihe nach für jedes Element aufgerufen wird. Um zu zeigen wie das geht, betrachten wir ein kleines Codebeispiel, bei dem der .upto-Iterator von Integer zum Einsatz kommt, um für die Zahlen von Eins bis Zehn eine Ausgabe zu erzeugen:
#!/usr/bin/ruby
Max = 10 # Konstante
1.upto(Max) do
|i| # iteriertes Element
print "#{i} ^2 = ", i*i, "n"
end
In der ersten Zeile definieren wir eine Konstante Max, deren Name mit einem Großbuchstaben beginnt, .upto ist der bereits erwähnte Iterator, der hier von 1 bis Max iteriert. do und end schließen den Codeblock ein, der an den Iterator übergeben wird. Alternativ kann man den Block auch mit { und } einklammern. |i| weist das Element der aktuellen Iteration der Variablen i zu, die print-Zeile gibt schließlich i und i*i aus. Beachten Sie, dass normale Variablen mit einem Kleinbuchstaben beginnen müssen und Konstanten mit einem Großbuchstaben. Falls Ihnen der Einwand auf der Zunge liegt, dass for viel universeller ist, gebe ich Ihnen Recht. Da man jedoch in neun von zehn Fällen mit einem Inkrement von eins rauf- oder runterzählt, ist die upto-Methode (und downto, step und andere) ein echter Gewinn. Wie wir gleich an einem etwas komplexeren Beispiel sehen werden, erlauben Iteratoren auch in vielen anderen Fällen eine sehr effiziente Programmierung. Betrachten Sie aber vorher noch einmal die Zeile Max = 10. Wir sollten uns noch einmal vergegenwärtigen, dass hier die 10 nicht einfach eine Zehn ist, sondern eine Instanz der Integerklasse. Der Zuweisungsoperator erzeugt eine neue Instanz und weist Max schließlich eine Referenz darauf zu. Betrachten wir eine etwas realere Problemstellung: Sie wollen im aktuellen Verzeichnis alle Dateien, die auf .MP3 enden, so umbenennen, dass sie auf .mp3 enden. Da das mv-Kommando von Linux bei diesem alltäglichen Problem kaum eine Hilfe ist (außer Sie sprechen fließend Bash), bauen wir uns ein Skript in Ruby. Die Aufgabe: Man hat ein Verzeichnis, will den Inhalt auslesen und je nach Beschaffenheit des Eintrags die Datei umbenennen. Während man sich in Perl mit Funktionen wie opendir und readdir herumschlagen muss, kann man das Problem in Ruby genau so lösen, wie wir es formuliert haben: Man nimmt ein Dir-Objekt, lässt sich die Einträge geben und benennt gegebenenfalls Dateien um. Betrachten Sie also den folgenden Code:
Dir.open(".").entries.each do
|entry|
neu = entry.gsub(/.MP3$/, ".mp3");
File.rename(entry,neu) if neu != entry
end
Das ist doch recht übersichtlich: Dir.open liefert eine neue Instanz der Klasse Dir, die wir nicht einer Variablen zuweisen, sondern direkt weiterverwenden. Die entries-Methode der Dir-Instanz liefert ein Array mit allen Verzeichniseinträgen in Form von String-Objekten, each schließlich ist der Standard-Iterator über alle Elemente eines Containers (hier: Arrays). Alle Elemente des Arrays landen nacheinander in der Variablen entry. Die Stringmethode gsub ersetzt .MP3 durch .mp3 und das Ergebnis landet in der Variablen neu. Die letzte Zeile schließlich benutzt die Klassenmethode rename der Klasse File, um gegebenenfalls eine Umbenennung vorzunehmen. Klassenmethoden lassen sich im Gegensatz zu Instanzmethoden ohne vorhandene Instanz einer Klasse aufrufen. Das angehängte if expr ist ein von Perl her bekanntes Konstrukt, bei dem sich mitunter die Nackenhaare sträuben. Man hätte natürlich ein normales dreizeiliges Konstrukt if, Kommandos, end einsetzen können, jedoch ist die gewählte Schreibweise vor allem bei einzeiligen if-Blöcken oftmals übersichtlicher. Auch wenn das Beispiel im ersten Moment befremdlich gewirkt haben mag, orientiert sich die Realisierung doch sehr daran, wie man denkt – und entsprechend schnell gewöhnt man sich an diese Art zu programmieren. Erwähnt sei hier noch die Möglichkeit, kleine Programme on the Fly mit dem -e `…`-Parameter an den Interpreter zu übergeben. Dann muss man allerdings die einzelnen Kommandos durch Semikola trennen.
Eigene Klassen
Nachdem wir in zwei einfachen Beispielen die Benutzung von Standard- Klassen und -Iteratoren gesehen haben, werden wir uns nun anschauen, wie man in Ruby selbst Klassen und später Iteratoren definiert. Um auch speziellere Eigenschaften von Klassen in Ruby zu zeigen, betrachten wir die Implementierung einer Klasse, deren Instanzen drei stabile Zustände einnehmen können: die Tribble-Klasse. Warum keine gängige Sprache einen derartigen Datentyp vorzuweisen hat, ist schon merkwürdig. Solche Klassen sind nämlich keineswegs unsinnig, kann man doch beispielsweise mit einer Klasse und den Zuständen soll, muss und egal recht gut die Wünsche eines Kunden in Bezug auf einen Artikel ausdrücken – um dann den optimalen Gegenstand für genau diesen Kunden herauszusuchen. Oder wie wäre es beispielsweise mit einer Klasse, die uns durch die Zustände ja, nein und jein bei schwierigen Entscheidungen hilft? Im Folgenden definieren wir zuerst eine allgemeine Klasse Tribble, von der wir dann zu Testzwecken die Klasse JNTribble ableiten, die uns bei Entscheidungen helfen wird. Betrachten Sie Listing 1 für eine erste einfache Klasse Tribble.
Listing 1: Die Tribble-Klasse |
class Tribble # Großbuchstabe vorne!
def initialize # Konstruktor
@zustaende = Array.new # mögl. Zustände
@zustand = nil # default-Zustand
end
def setValues( arr ) # erlaubte Werte eintragen
if (arr.type.to_s == "Array")
arr.uniq! # doppelte Elemente killen
@zustaende = arr if arr.size == 3
end
end
protected :setValues
def get # Methode, Auslesen d. Zustands
@zustand
end
def set( neu ) # Methode, Zustand setzen
@zustand = neu [email protected]?(neu)
end
def ==(other) # Vergleichsoperator
return nil if get.nil? or other.get.nil?
@zustand == other.get
end
end
|
Der Name der Klasse muss mit einem Großbuchstaben beginnen. Mit def definiert man eine Methode, initialize ist der reservierte Name für den Konstruktor. Dort legen wir die Instanzvariablen @zustaende (Möglichkeiten) und @zustand (aktueller Zustand) an, auf die innerhalb einer Instanz alle Methoden zugreifen können. Die Methode setValues dient dazu, die möglichen Zustände des Tribbles festzulegen. Diese Methode sei protected, damit sie nur in Tribble und abgeleiteten Klassen benutzbar ist. .get dient dazu, den aktuellen Zustand auszulesen, .set(wert) setzt den aktuellen Zustand. In .get steht kein return, es kann weggelassen werden, da der Wert des letzten Ausdrucks automatisch der Rückgabewert der Methode ist. Die dritte Methode schließlich definiert den ==-Operator für die Tribble-Klasse: Ist der Zustand eines der Tribbles nil, dann ist auch das Ergebnis nil, sonst werden die Zustände der beiden beteiligten Tribbles verglichen und als Ergebnisse true beziehungsweise false zurückgegeben. Die .nil?-Methode ist in der Klasse Object definiert und liefert true nur dann, wenn das Objekt, dessen nil?-Methode aufgerufen wird, nil ist. Anders gesagt: Eine Variable ist niemals undefiniert, sondern mindestens eine Instanz der nil-Klasse. Um eine verwendbare Klasse zum Spielen zu haben, leiten wir von Tribble die neue Klasse JNTribble ab, die uns mit Analysefunktionen das Leben leichter macht. Die neue Klasse soll die Funktionalität der Tribble-Klasse haben und die Zustände ja, nein und jein einnehmen können. Bei Ruby darf eine Klasse jeweils nur eine andere Klasse beerben. Um weitere Methoden in einer Klasse verfügbar zu machen, könnte man Module einbinden, die außerhalb der Klasse definiert sind. In unserer neuen Klasse (Listing 2) definieren wir einen neuen Konstruktor, der den Konstruktor der Elternklasse aufruft und die möglichen Zustände von Instanzen unserer Klasse festlegt. Dabei kann optional ein Startzustand angegeben werden, default ist ja. Außerdem definieren wir einen neuen Operator +, der zwei Instanzen von JNTribble verknüpft und abhängig von den Zuständen eine neue JNTribble-Instanz liefert. Die Logik lässt sich durch die Verwendung von Statement-Modifiers in drei Zeilen unterbringen: Wenn zwei zu vergleichende Tribbles ja sind, ist das Ergebnis ja, aus ja + nein wird jein und aus dem Rest nein. Damit steht nun eine verwendbare Tribble-Klasse für erste Tests zur Verfügung. Dazu instanzieren wir das JNTribble zweimal und vergleichen die Inhalte der Objekte:
t1 = JNTribble.new("ja")
t2 = JNTribble.new("nein")
print "Vergleich ", t1 == t2, "n"
print "ja + nein = "
t3 = t1 + t2 # ja + nein = ?
puts t3.get
Die Ausgabe ist:
Vergleich: false ja + nein = jein
Ist ja klar, denn ja + nein ergibt nun mal ein glasklares jein, was sonst. Die Anweisung puts gibt den übergebenen String gefolgt von n aus.
Iteratoren im Eigenbau
Wo bleiben die versprochenen Iteratoren? Nun, wir bauen uns jetzt einen Container für JNTribbles, HTArray, mit einem Iterator each, der über alle Elemente (Tribbles) iterieren kann, ohne den Anwender mit lästigen Details über die interne Speicherung der Tribbles zu belästigen. Außerdem bauen die beiden Methoden add(object) und get(num) zum Einspeichern beziehungsweise Auslesen von Tribbles ein. Listing 3 zeigt eine einfache Implementierung, die nicht nur Tribbles einspeichern würde (Arrays können beliebige Objekte speichern) und die man in diesem Fall eigentlich auch durch ein normales Array ersetzen könnte. Die Verwendung einer eigenen Klasse mit Iterator hat jedoch den Vorteil, dass wir später problemlos die klasseninterne Realisierung der Speicherung ändern könnten, während die Schnittstelle nach außen gleich bliebe.
Listing 2: JNTribble – Das definierte Jein |
class JNTribble < Tribble # erbt alle Methoden def initialize(zustand="ja") # optionaler defaultwert super() # ruft die methode der Elternklasse auf. setValues( ["ja", "nein", "jein" ] ) # Array on the fly... @zustand = zustand [email protected]?(zustand) end def +(other) # ein selbstdefinierter Operator: t2 = t1 + t2 return JNTribble.new("ja") if (<@>zustand == "ja") and (other.get == "ja") return JNTribble.new("jein") if (<@>zustand == "ja") or (other.get == "ja") return JNTribble.new("nein") end end |
Die Definition der Iterator-Methode ist relativ simpel: Man schreibt eine Methode, die nacheinander alle gespeicherten Objekte an die Hand nimmt und an das yield-Kommando übergibt. Bei der Verwendung des Iterators tritt dann für jedes Objekt der übergebene Codeblock an die Stelle des yield, wobei die Parameter des yield-Kommandos in den in |…| angegebenen Variablen landen.
Listing 3: Ein Array aus Tribbles |
class HTArray
def initialize
@arr = Array.new # Daten in Array
end
def add( tribble ) # tribble anhängen
@arr.push( tribble )
end
def get ( num ) # tribble auslesen
@arr[num]
end
def each # Iterator über alle tribbles
@arr.each {
|tribble|
yield tribble # block mit tribble aufrufen
}
end
end
|
Listing 3 zeigt eine mögliche Implementierung von HTArray mit einem .each-Iterator. Mit folgendem Codeschnipsel können Sie das HTArray ausprobieren. Wenn Sie die Listings zu JNTribble und HTArray in einzelnen Dateien abgelegt haben, müssen Sie die Klassendefinitionen mittels require einbinden:
require `jntribble.rb'
require `htarray.rb'
ha = HTArray.new
ha.add( JNTribble.new("ja") )
ha.add( JNTribble.new("jein") )
ha.add( JNTribble.new("nein") )
print "Vergleiche `ja' mit allen Werten:n"
ha.each {
|t| # yield tribble, tribble -> t
print ((t + JNTribble.new("ja")).get, "n")
}
Vorher hatten wir die Klammern bei print weggelassen, sobald jedoch die Ausdrücke hinter print kompliziert werden, sollte man nicht auf Klammern verzichten, damit der Interpreter weiß, welcher Teil des Textes hinter print ein Parameter für den Aufruf ist. In Zweifelsfällen lassen sich mit Hilfe von ruby -w missverständliche Stellen im Quellcode anzeigen.
Kontrollierter Ausnahmezustand: Exceptions
Bei der Interpretation von Ruby-Programmen kann es vorkommen, dass Typumwandlungen scheitern (wenn eine Variable unvorhergesehen nil ist) oder dass eine auszulesende Datei überraschend nicht vorhanden ist. In diesen Ausnahmesituationen wirft der Interpreter so genannte Exceptions (Ausnahmen), die auch von Java und C++ her bekannt sind. Fängt man eine Exception nicht selber ab, wird das Programm automatisch beendet. Das ist jedoch nicht unbedingt im eigenen Interesse, da dadurch beispielsweise umständlich berechnete Werte oder langwierige Benutzereingaben verloren gehen können. Daher kann man Exceptions im Programm abfangen, um passend zu reagieren. Das folgende kleine Beispiel reagiert beim Speichern eines Ergebnisses auf das Problem, dass die Datei mitunter nicht schreibbar ist. Dieser Effekt tritt gelegentlich auf, wenn man ein Programm aus dem Home-Verzeichnis eines anderen Users startet, der den Namen der Zieldatei fest verdrahtet hat. Exceptions fängt man mittels begin / recue / end:
resfile = "/home/root/.result" begin file = File.open(resfile, "w") # Daten speichern rescue puts "Fehler!" print "Geben Sie eine schreibare " print "Datei für die Ergebnisse an: " resfile = STDIN.gets retry # nommaaal! ensure file.close # Datei zu end
Wenn resfile nicht schreibbar ist, wirft File.open(…,”w”) eine entsprechende Exception. rescue fängt sie auf – und wir versuchen, das Problem zu umgehen: Der User wird aufgefordert, einen neuen Dateinamen einzugeben, der über STDIN.gets eingelesen wird. retry bewirkt, dass der kritische Block ein weiteres Mal durchlaufen wird. In diesem Beispiel ginge das endlos im Kreis, bis der User schließlich den Namen einer schreibbaren Datei angibt. Der Code nach ensure wird auf jeden Fall ausgeführt. Benutzt man ensure und rescue, muss rescue zuerst erscheinen. Exceptions abfangen ist eine Sache – aber natürlich kann man auch selbst Exceptions werfen. Das geschieht einfach über den Befehl raise, dem man einen beschreibenden String mitgibt. Den kann man dann über $! in der Exception-Behandlung auslesen:
begin
raise "unerwarteter Fehler"
rescue
print "Exception!: ", $!, "n"
print "(",$!.type,")n"
end
Exceptions sind Instanzen von Exception-Klassen, die alle von Exception abgeleitet sind. Diese wiederum sind Kinder der Mutter aller Objekte ( Object) und verfügen somit über die Methode .type, mit der sich der Name der Klasse einer Instanz auslesen lässt. Wir können also in der Ausnahmebehandlung den Typ der Exception erfahren:
begin a = 1 / 0 rescue print $!.type, " : ", $!, "n" end
Im ersten Beispiel sah es noch so aus, als sei $! einfach ein String. Aber offensichtlich ist es eine Klasseninstanz und print hat irgendwie eine passende Methode von $! aufgerufen, um den String anzuzeigen. Es sollte uns jetzt nicht mehr überraschen, dass es sich dabei um die Standardmethode .to_s handelt.
Grafisches Allerlei
Falls die bislang beschriebenen Eigenschaften den Eindruck entstehen ließen, Ruby sei eine reine Konsolen-Sprache – dem ist nicht so. Ruby hat eine eingebaute Unterstützung für Tk, mit der der kundige Programmierer zumindest einfache GUIs schnell zusammenstricken kann. Ein richtiges grafisches GUI-Zusammenklick-Tool gibt es leider noch nicht, allerdings immerhin schon eine Schnittstelle zu Glade, dem Gtk-GUI-Builder. Und natürlich eine direkte Schnittstelle zu Gtk+, die man samt Dokumentation auf [5] findet. Für die Programmierung von Tk sollte man auf jeden Fall einen Blick in die Ruby-Pflichtlektüre werfen, nämlich die downloadbare Online-Version des kürzlich erschienenen Buches “Programming Ruby” [3], die unter [4] bereitsteht.
Alles zu Ruby online
Das gedruckt ziemlich teure Buch gibt es derzeit nur in Englisch. Es ist recht aktuell, was auf die offizielle Referenzdokumentation leider nicht unbedingt immer zutrifft. Ruby ist halt eine japanische Entwicklung und eine aktuelle Dokumentation erscheint meist erst in Japanisch, um dann mit etwas Glück ins Englische übersetzt zu werden. Es sind sogar die XML-Quellen des Buchs verfügbar. Der XML-Code ist allerdings hässlich, wie einer der Autoren bestätigte, der den XML-Code aus TeX-Code extrahiert hat. Immerhin kann man sich aus den XML-Dateien recht passable Kurzreferenzen basteln oder das Buchlayout an eigene Vorstellungen anpassen. Passender XSLT-Code für die Umwandlung liegt dem Buch leider nicht bei – man muss sich da schon selbst etwas ausdenken. Beispielsweise mit Hilfe der Klasse xmlparser oder einer der anderen XML-Erweiterungen, die man im Ruby Application Archive [6] oder auf Ruby Mine [7] findet. Dabei empfiehlt es sich oft, nicht dem Downloadlink direkt zu folgen, sondern auf der Homepage der jeweiligen Erweiterung nach einer neueren Version zu suchen, da jeder Autor zuerst seine Homepage updatet.
Der Rest – wie er im Buche steht
Kommen wir zum Ende des Artikels. Ein paar Dinge wurden nicht angesprochen – zum Beispiel Threads -, da dieser Artikel kein Buch ersetzen soll. Informationen zu Threads und zum Rest von Ruby findet man natürlich im erwähnten Online-Buch und vielleicht auch in diversen anderen Online-Dokumenten, die mit jeder guten Suchmaschine im Netz zu finden sein sollten. Eine aktuelle Referenz erhält man über das Tool ri, das im Application Archive zu haben ist. Der Aufruf ri File spuckt beispielsweise alle Methoden der File-Klasse aus. Der Konsolen-Einzeiler ruby -e `puts File .methods.join(“n”) tut das zwar auch, allerdings ohne nähere Erklärungen. Für weitere Informationen ist auch die englische Ruby-Mailingliste gut (Infos auf der Ruby-Homepage). Wer dort mitliest, behält den Überblick über aktuelle Entwicklungen und kann sogar an der weiteren Entwicklung teilhaben, da “Matz” Matsumoto vor Änderungen erst mal in der ML nachfragt. Flatrate- und sonstige Dauer-Surfer können schließlich noch im IRC über den Server irc.openprojects.net den Kanal #ruby-lang ansteuern, in dem sich stets ein paar Ruby-Enthusiasten finden, unter anderem auch Autoren des Online-Buchs. Wer sich weiter mit Ruby beschäftigt, sollte sich auf jeden Fall noch den Unterschied zwischen Codeblocks von Iteratoren und dem Code innerhalb von while, until und if anlesen – und welche der Variablen lokal veränderbar sind oder auch nicht. Viel Spaß dabei. ( uwo)
Infos |
| [1] Welche Sprache ist besser?: http://www.perl.com/pub/2000/12/advocacy.html [2] Ruby Homepage: http://www.ruby-lang.org/en/ [3] Thomas, Hunt, Matsumoto: Programming Ruby; Addison-Wesley; ISBN 0201710897 [4] Online-Version des Buchs: http://www.pragmaticprogrammer.com/ruby/ [5] ruby-gtk: http://www.ruby-lang.org/gtk/en/ [6] Ruby Application Archive: http://www.ruby-lang.org/en/raa.html [7] Ruby Mine: http://www.ale.cx/mine/raa.html [8] Ruby-FAQ: http://www.ruby-lang.org/ [9] Open Directory Project: http://www.dmoz.org/Computers/Programming/Languages/Ruby |
Der Autor |
| Tjabo Kloppenburg studiert genau genommen E-Technik an der Uni Siegen, hat sich jedoch in Richtung IT spezialisiert. Gibt es etwas Schöneres, als IT-Skripte zu basteln? Er findet: nein. |





