Unser Artikel erschien in Linux Enterprise 10/2001. Wir danken Nadja Rosmann von Linux Enterprise für die Erlaubnis, den Artikel online zugeben.

Funkelndes Juwel

Armin Roehrl und Stefan Schmiedl

Ruby ist eine relativ neue, interpretierte Sprache, in der zum einen (kleine) Skripte erstellt werden können, aber auch größere Projekte objektorientiert wachsen und gedeihen. Der große Vorteil gegenüber Perl ist, dass die Objekte nicht aufgepfropft sind, sondern ihre Wurzeln tief in der Sprachstruktur verankert haben.

Wir wollen in diesem einführenden Artikel die Stärken von Ruby aufzuzeigen und in einem Nachfolger dann einige kleine Projekte vollständig entwickeln, um die Flexibilität und Effizienz von Ruby-Skripten zu demonstrieren.

Nicht noch eine Programmiersprache!

Rubys Abstammung ist ziemlich exotisch. Yukihiro "Matz" Matsumoto hat sie seit 1993 entwickelt, in ihrer japanischen Heimat ist sie schon lange populär, beliebter jedenfalls als Python. Ruby ist als Open Source frei verfügbar [1], mittlerweile gibt es auch englische Literatur, deutsche Bücher sind im Entstehen begriffen. Matz wollte seine neue Sprache wie Perl nach einem Juwel bennenn und er entschied sich für Ruby nach den Geburtsstein eines Kollegen. Später merkte er erst, dass Ruby als Nachfolger von Perl prädestiniert ist: im Alphabet, bei Geburtssteinen (Perle = Juni, Rubin = Juli) [4].

Ruby wird zwar hauptsächlich unter Linux entwickelt, läuft aber auch auf anderen Unix Varianten, DOS, Windows 95/98/NT, Mac, BeOS, OS/2, etc.

Installation und Aufruf

Die Installation ist einfach: "./configure && make install" auf Unix-Systemen bzw. der Aufruf eines Installers unter Windows bietet keine Überraschungen.

Wer nur mal schnell reinschnuppern will, kann unter [11] den Online-Interpreter ausprobieren, ohne Ruby auf dem eigenen Rechner installieren zu müssen.

Ruby läuft (wie auch Perl und Python) in einer Konsole. Mit "ruby skript.rb" wird das genannte Skript ausgeführt, Freunde von Einzeilern werden "ruby -e '...'" zu schätzen wissen. Mit "irb" schließlich steht eine interaktive Ruby-Shell zur Verfügung, in die die Beispiele weiter unten ohne Probleme eingetippt werden können.

Ruby's Glanz

Man nehme das Beste von Smalltalk, Perl, Eiffel, Scheme, CLU, Sather und Lisp, mische mehrmals stark und warte, bis sich etwas herauskristallisiert: Ruby. Von der Objektstruktur her ist Ruby näher an Smalltalk als an Lisp, auch die mächtigen Lispmakros fehlen, obwohl durch eval wieder Einiges wettgemacht wird.

Matz hat Ruby als seine "ideal Sprache" von Anfang an nach den Prinzipien des Mensch-Maschinen Interfaces designt [6]:

Objekte und Klassen

Ruby zeichnet sich durch eine einfache Syntax aus, die teilweise an Eiffel und Ada erinnert. Befehle enden nicht mit Strichpunkten, wenn nur ein Kommando pro Zeile steht. Kommentare verwenden wie in Perl das #. Ebenso wie in Java oder Python sind Exceptions in Ruby integriert, wodurch die Behandlung von Ausnahmesituationen enorm vereinfacht wird.

Ruby's Operatoren sind "nur" syntaktische Alternativen für normale Methoden, sie lassen sich bei Bedarf leicht umdefinieren. Wie Smalltalk ist Ruby eine rein objektorientierte Sprache, mit Ausnahme der Kontrollstrukturen. Dafür hat Ruby aber auch eine Syntax die von den meisten Programmierern ohne Schwierigkeiten gelesen werden kann.

Zum Beispiel ist die Zahl 1 ein Ruby eine Instanz der Klasse Fixnum. Funktionsaufrufe sind stets Botschaften, die an ein Objekt geschickt werden. Der Empfaenger steht links, die Funktion rechts vom Punkt:

    -4.abs() # -> 4

Die vollständige Klassenstruktur ist offen für Erweiterungen, sogar während der Laufzeit. Es gibt also keine Probleme mit versiegelten Klassen, die nicht mehr geändert werden können. Falls nötig, kann so eine Instanz einer Klasse (Singleton) sich anders verhalten als eine andere Instanz derselben Klasse.

Für die Deklaration einer Methode wird das Schlüsselwort def verwendet, gefolgt vom Funktionsnamen und einer Argumentenliste, für die Parameter können auch Standardwerte gesetzt werden.

    def eineMethode(x, y=7)
      ...
    end

Tritt def außerhalb einer Klasse auf, bekommt man eine "gewöhnliche" Funktion, ähnlich wie Perls "sub"-Routinen.

Klassendeklarationen beginnen mit "class", Instanzen werden mit der "new()"-Methode erzeugt.

    class Krokodil
      def essen() 
        p "Haps"
      end
    end
    k=Krokodil.new()
    k.essen()        # -> haps

Bewusst unterstützt Ruby nur einfache Vererbung (single inheritance). Vererbung wird durch < gefolgt von der Basisklasse angezeigt.

    class Krokodil < Tiere
      ...

Durch Module (Kategorien in Objective-C) werden Methoden und Konstanten gegliedert. Eine Klasse kann ein Modul importieren und bekommt so dessen Methoden. Dieses "Mix-in"-Verfahren ermöglicht es, die Vorteil der Mehrfachvererbung zu nutzen, ohne sich mit ihrer Komplexität aufzuhalten. Im Unterschied zu reinen Interfaces wie in Java enthalten Module auch die Implementation.

Code-Blöcke

Ruby ermöglicht auch das Arbeiten mit Code-Blöcken. Ein Block wird durch geschweifte Klammern '{ ... }' oder 'do ... end' begrenzt und kann als zusätzliches Argument an eine Methode übergeben werden. Wer sich einmal an das Arbeiten mit solchen anonymen Blöcken gewöhnt hat, wird sie nicht mehr missen wollen.

    def dreimal
      yield
      yield
      yield
    end
    dreimal { puts "Hi" }  # -> drei mal Hi
    dreimal { puts "Ho" }  # -> drei mal Ho
Yield wird mächtiger, sobald man an Blöcke Argumente übergibt und Werte zurück bekommt. Hier dringt ein bisschen Smalltalk-Syntax durch:
    def findnums(n)
      i=0
      while i 0 3 6 9 12 15 18 

Blöcke in Verbindung mit Iteratoren ermöglichen schließlich sehr elegante Formulierungen. Analog dem Visitor-Pattern besucht der Block jedes Element der Liste:

    [1,25,30].each {|i| puts i} # -> 1 25 30

Ruby hat auch echte "closures" wie Lisp, die sich den Kontext (Variablen-Bindungen zum Zeitpunkt ihrer Definition) merken.

    def nTimes(aThing)
      return proc{ |n| aThing * n }
    end

    p1=nTimes(23)
    p1.call(1)  -> 23
    p1.call(3)  -> 69
Mit "proc" wird aus einem Block eine Funktion gemacht, die als Wert von nTimes zurückgeliefert wird. Diese Funktion hat auf den Wert des aThing-Parameters zum Zeitpunkt des nTimes-Aufrufs Zugriff.

Unabhängige Zähler lassen sich mit problemlos erzeugen:

    def counter(start)
      return proc { start += 1 }
    end

    f = counter(0)
    g = counter(10)

    p f.call # -> 1
    p f.call # -> 2
    p g.call # -> 11
    p f.call # -> 3

Abfallsammler

Ruby besitzt einen true "mark-and-sweep" Garbage Collector, der auf alle Ruby Objekte wirkt. Sobald ein Objekt nicht mehr von einem anderen Objekt referenziert wird, wird es beseitigt. Deshalb hat Ruby wie Java im Gegensatz zu C++ keine Destruktoren. Der Vorteil dieses Verfahrens ist, dass das leidige Referenzzählen entfällt, das bei manchen Perl- und Python-Programmierern schon Haarausfall verursacht haben soll.

Auf Grund des garbage collectors und des ausgefeilten Extension-APIs kann Ruby sehr einfach durch C-Funktionen erweitert werden. Sinnvoll ist das z. B. bei aufwändigen numerischen Berechnungen. Neben dieser "handwerklichen" Methode gibt es auch noch SWIG (Simplified Wrapper and Interface Generator) [7], der die Einbindung von C-Funktionen fast vollständig automatisiert.

Zahlen zählen

In Ruby braucht man sich um die interne Repräsentation von Integern keine Sorgen machen. Es gibt kleine Integer (Instanz der Klasse Fixnum) und grosse Integer (Bignum), die bei Bedarf ineinander umgewandelt werden, wovon der Programmierer in der Regel nichts merkt.

    def fact(n)
      1 if n == 0
      f = 1
      while n>0 
        f *= n
        n -= 1
      end
      f
    end

    print fact(ARGV[0].to_i) 
    # -> 10093326215443944152\
      68169923885626670049071\
      59682643816214685929638\
      95217599993229915608941\
      46397615651828625369792\
      08272237582511852109168\
      64000000000000000000000000

Variablen und Datentypen

Ruby ist eine der dynamischen Sprachen, bei denen die Variablen nur als Speicher für Objekte dienen, und damit keinen Datentyp benötigen. Die Objekte selbst haben sehr wohl unterschiedliche Typen.

Eine einfache Namenskonvention zeigt den Geltungsbereich von Variablen an:

    a   # lokal in der umgebenden Funktion
    $b  # global für das Skript

    @c  # lokal für die Instanz einer Klasse
    @@d # global für die Klasse
    E   # Klassenname

Auf eine Variable kann erst zugegriffen werden, wenn sie mit einem Wert initialisiert worden ist.

class Krokodil
  def initialize(name)
    @name=name       # versch. Namen
    @@anzahlZaehne=7 # immer 7 Zähne
  end
end 

Reguläre Ausdrücke

Reguläre Ausdrücke werden ähnlich wie in Perl und Python unterstützt, sie können sowohl mit den Perl-Kürzeln als auch mit Konstruktoren erzeugt werden. Das erzeugte MatchData-Objekt enthält in einem Vektor den kompletten Bereich, auf den das Suchmuster gepasst hat sowie die einzelnen geklammerten Komponenten:

    aString = "dass das daß jetzt dass ist ..."

    /da(ß|ss)/.match(aString)[0]             # -> daß
    Regexp.new('da(ß|ss)').match(aString)[0] # -> daß
    /da(ß|ss)/.match(aString)[1]             # -> ß
    Regexp.new('da(ß|ss)').match(aString)[1] # -> ß

Ersetzungen sind ebenso einfach möglich

    aString.sub('(da)(ß|ss)', '\1ss')

Reflexion

Ruby besitzt mächtige Reflexionsmechanismen, welche vor allem beim verteilten Einsatz für Marshalling von Interesse sind.

    x=43.2
    ObjectSpace.each_object(Numeric){|o| p o}
    # -> 43.2 2.718281828 3.141592654   

Die beiden letzten Zahlen sind Näherungswerte für E und PI, die im "Math"-Modul definiert sind und wir alle "lebenden" Objekte zeigen ließen.

Beispiele

Das obligatorische Hello World darf natürlich nicht fehlen:

    puts "Hello, World!"

In shebang-Notation:

    #!/usr/local/bin/ruby
    puts "Hello, World!" 

Stellvertretend für mehrere plattformübergreifende Bibliotheken wollen wir auch eine GUI-Version zeigen:

    require 'tk'
    root= TkRoot.new { title "Hallo" }
    TkLabel.new(root) {
      text 'Hello, World!'
      pack { padx 15; pady 15; side 'left' }
    }
    Tk.mainloop 

Benchmark: Fibonacci Zahlen

Eine nicht sehr repräsentative Benchmark, aber trotzdem interessant: Fibonnacci Zahlen sind nach der bekannten Formel f(n)=f(n-1)+f(n-2), mit f(0)=f(1)=1 Formel zu berechnen. Wir haben einige Programme von [12] übernommen und den Rest selber geschrieben.

#!/usr/local/bin/ruby
def fib(n)
  if n<2
    n
  else
   fib(n-2)+fib(n-1)
  end
end
print(fib(30), "\n")

Die Benchmarks wurden auf einer Pentium II mit 266MHz Linux(2.2.18) durchgeführt. (n=30)


    Sprache:   Laufzeit        Version
    Assembler  0.25 s          GNU assembler 2.10.91
    C          0.3 s           GCC 2.95.2
    Java       1.4 s           1.3.0_02
    Smalltalk  4.5 s           GST 1.95.4
    Python     22.6 s          2.0
    Ruby       26.5 s          1.6.3          
    Perl       38.0 s          5.6.0
    CLisp      44.6 s          2000-03-06
    PHP        52.6 s          4.0.4pl1    
    AWK        92.0 s          GNU 3.0.6
    Scheme     241.1 s         UMB Scheme 3.2
    tcl        440.5 s         8.3

Bei PHP muss dafür gesorgt werden, dass der Prozess nicht automatisch nach 30 Sekunden abbricht: "php -d max_execution_time=200" tut's.

Da die startup Zeit der verschieden Sprachen, wie z.b. der JVM erheblich ist, lassen noch einmal die gleichen Benchmarks laufen, jedoch mit einer Schleife, so dass alles 20 mal wiederholt wird. Assembler, AWK, tcl, etc. werden nicht mehr wiederholt.

    Sprache:   Laufzeit      
    C          6.2 s
    Java       7.3 s
    Smalltalk  1 m 47 s
    Python     7 m 54 s
    CLisp      12 m 18 s
    Perl       13 m 12 s
    Ruby       15 m 45 s
    PHP        18 m 45 s
    Scheme     23 m 40 s

Nicht rekursive Fibonnaci-zahlen für n=30 mit je 1000 Wiederholungen:

    Sprache:   Laufzeit      
    C          0.01 s
    Perl       0.2 s
    Smalltalk  0.2 s
    Python     0.4 s
    Ruby       0.5 s
    CLisp      0.7 s
    PHP        0.9 s
    Java       1.3 s
    Scheme     1.7 s
    tcl        3.7 s

Worthäufigkeiten zählen

Bisher haben wir den gleichen Algorithmus in den verschienden Sprachen benuetzt, aber das ist unrealistisch, da man in verschiedenen sprachen verschiedene Stärken ausspielen kann. Folglich haben wir einen bekannten Test "Word Frequency Count" benuzt. Siehe [13] für noch mehr verschiedenen Sprachen oder [14] für eine andere Anwendung.

Als Eingabetext wurde das 170KB lange "The Prince" von Nicoló Machiavelli verwendet, welcher 20 mal wiederholt wurde, i.e. eine Datei von 20*176KB=4.2MB Länge.

Sprache   Laufzeit
C         3.7
Perl      8.7   
Java      10.8  
Python    23.3
Bash      23.3  
Ruby      29.3
tcl       64.6

Dieser Test ist ein Prüfstein für die Implementation der Hashtabellen in Perl, Java, Python und Ruby. Wie alle Benchmarks sollte er mit Vorsicht genossen werden, weil z. B. nicht mit eingeht, wie lange der Programmierer gebraucht hat, um die notwendigen Funktionen zu schreiben.

Man wählt Ruby ist sicherlich nicht auf Grund der Geschwindigkeit des ausführbaren Codes, sondern wegen der Einfachheit und Eleganz der Lösungen. Bei Bedarf können rechenintensive "Kleinteile" in C optimiert werden. Aber auch ohne Erweiterungen spielt Ruby in der gleichen Liga wie Perl und Python.

Unterschied zu Java:

[10] gibt einen ausführlichen Vergleich von Ruby mit anderen Computer sprachen.

... der Geist ist aus der Flasche

Lange Zeit gab es nur japanische Webseiten und Bücher, mittlerweile verbreiten Ruby-Enthusiasten die Sprache aber weltweit in immer stärkerem Maße. Dokumentationen sind mittlerweile auch in nicht-fernöstlichen Sprachen verfügbar, siehe [3] und [5]. Wir arbeiten gerade mit Clemens Wyss an einem deutschen Rubybuch [15] für den dpunkt-Verlag, das noch 2/2002 erscheinen wird.

Von RubyUnit [2] über native Windows Calls, COM, CGI scripts, eingebettetem Ruby in HTML Seiten (eRuby, entspricht ASP, JSP oder PHP) bis zu mod_ruby [8] (Ruby Interpreter direkt im Apache Server) unterstuezt Ruby bereits alles, was das Herz sich wuenschen kann. Ruby besitzt eine sehr aktive Entwicklungsgemeinde, auch die Newsgruppe comp.lang.ruby ist sehr aktiv.

Literatur/Links

[1] www.ruby-lang.org
[2] Rubyunit, http://homepage1.nifty.com/markey/ruby/rubyunit/index_e.html
[3] Programming Ruby by David Thomas und Andrew Hunt, http://www.rubycentral.com/book/index.html
[4] Ruby FAQ; http://www.rubycentral.com/faq/index.html
[5] www.rubycentral.com a complete online reference to Ruby's built-in classes and methods.
[6] Dr. Dobb's Journal, January 2001, "Programming in Ruby"
[7] SWIG, http://www.swig.org
[8] modruby, http://www.modruby.net/
[9] Larry Wall interview http://lwn.net/2001/features/LarryWall/
[10] Vergleich mit anderen Sprachen. http://www.ruby-lang.org/en/compar.html
[11] Online Ruby Interpreter http://www.ruby.ch
[12] http://www.lionking.org/~cubbi/serious/fibonacci.html
[13] http://www.bagley.org/~doug/shootout/bench/wordfreq/
[14] http://www.theregister.co.uk/content/4/20703.html
[15] Dt. Ruby-Buch: http://www.dpunkt.de/ruby

Zusammenfassung:

Ganz zum Schluss noch was zum Abkühlen, weil es gerade 01:30 ist und draußen immer noch 25 Grad rumschwülen:

99.downto(0){|x|w=" on the wall";u=" #{x>0?(x):'no more'}
bottle#{x!=1?'s':''} of beer";p u+w+u+". Take one down, pass it
around,"+u+w}


Ruby home
Linux Enterprise
RubyChannel
Articles/slides/tutorials.
Ruby slides.
Back to Approximity.