Programmierung in Ruby

Der Leitfaden der Pragmatischen Programmierer

Wenn's Schwierigkeiten gibt



Es ist traurig aber wahr, es ist möglich in Ruby fehlerhafte Programme zu schreiben. Das tut uns sehr leid.

Aber keine Angst! In Ruby gibt es mehrere Teile, die dir beim Debuggen deiner Programme helfen. Wir werden uns diese Teile ansehen und dann zeigen wir ein paar übliche Fehler, die man machen kann, und wie man sie beseitigt.

Der Ruby-Debugger

Mit Ruby wird ein Debugger geliefert, der ganz bequem in das Basis-System mit eingebaut ist. Man führt den Debugger aus, indem man den Interpreter mit der Option -r debug startet, zusammen mit allen anderen sonst noch nötigen Optionen und dem Namen des Skripts:

ruby -r debug [options][programfile][arguments]

Der Debugger besitzt all die üblichen Merkmale, die man so erwartet, inklusive der Möglichkeit, Breakpoints zu setzen, in Methoden hinein- oder über sie hinwegzulaufen sowie das Anzeigen des Stack-Bereichs und der Variablen.

Er kann weiterhin die für ein bestimmtes Objekt oder eine Klasse definierten Instanz-Methoden auflisten und man kann mit ihm verschiedene Threads innerhalb von Ruby auflisten und kontrollieren. Tabelle 12.1 auf Seite 133 zeigt alle Kommandos, die mit dem Debugger möglich sind.

Falls readline-Support angeschaltet ist, kann man mit dem Cursor vorwärts und rückwärts in der Kommando-History springen und man kann vorherige Eingaben zeilenweise ändern.

Um ein wenig zu zeigen, wie der Ruby-Debugger arbeitet, hier ein üblicher Ablauf.

% ruby -rdebug t.rb
Debug.rb
Emacs support available.
t.rb:1:def fact(n)
(rdb:1) list 1-9
[1, 10] in t.rb
=> 1  def fact(n)
   2    if n <= 0
   3      1
   4    else
   5      n * fact(n-1)
   6    end
   7  end
   8
   9  p fact(5)
(rdb:1) b 2
Set breakpoint 1 at t.rb:2
(rdb:1) c
breakpoint 1, fact at t.rb:2
t.rb:2:  if n <= 0
(rdb:1) disp n
  1: n = 5
(rdb:1) del 1
(rdb:1) watch n==1
Set watchpoint 2
(rdb:1) c
watchpoint 2, fact at t.rb:fact
t.rb:1:def fact(n)
1: n = 1
(rdb:1) where
--> #1  t.rb:1:in `fact'
    #2  t.rb:5:in `fact'
    #3  t.rb:5:in `fact'
    #4  t.rb:5:in `fact'
    #5  t.rb:5:in `fact'
    #6  t.rb:9
(rdb:1) del 2
(rdb:1) c
120

Interaktives Ruby

Wenn man mit Ruby spielen will, benutzt man eine Funktionalität mit Namen Interaktives Ruby, kurz irb. Irb ist im Wesentlichen eine Ruby-Shell und im Konzept ähnlich wie eine Betriebs-System-Shell (inklusive Job Control). Das liefert eine Umgebung, in der man in Echtzeit mit der Sprache ``herumspielen'' kann. Man startet irb an der Kommandozeile mit:

irb [irb-options][ruby_script][options]

Irb stellt den Wert jedes abgeschickten Ausdrucks dar. Als Beispiel:

% irb
irb(main):001:0> a = 1 +
irb(main):002:0* 2 * 3 /
irb(main):003:0* 4 % 5
2
irb(main):004:0> 2+2
4
irb(main):005:0> def test
irb(main):006:1> puts "Hello, world!"
irb(main):007:1> end
nil
irb(main):008:0> test
Hello, world!
nil
irb(main):009:0> 

Irb gibt einem außerdem die Möglichkeit, Subsessions zu erzeugen, jede mit einem eigenen Kontext. Man kann zum Beispiel eine Subsession erzeugen mit demselben (top-level) Kontext wie die Original-Session oder eine Subsession im Kontext einer speziellen Klasse oder Instanz. Das Beispiel aus Figur 12.1 auf Seite 128 ist ein wenig länger, aber es zeigt, wie man Subsessions erzeugen und zwischen ihnen springen kann.

irb-Beispiel-Session

Figur 12.1 irb-Beispiel-Session

Eine komplette Beschreibung aller Kommandos, die irb unterstützt, gibts ab Seite 523.

Wie beim Debugger kann man, wenn die Version von Ruby mit GNU-Readline-Support erzeugt wurde, die Pfeil-Tasten benutzen (wie beim Emacs) oder vi-ähnliche Tasten-Kombinationen, um einzelne Zeilen zu editieren oder um zurück zu gehen und eine frühere Zeile neu auszuführen --- gerade wie bei einer Command-Shell.

irb eignet sich prima zum Lernen: es ist sehr bequem, wenn man eine Idee schnell ausprobieren möchte, um zu sehen ob sie funktioniert.

Editor Support

Ruby wurde so entwickelt, dass es ein Programm in einem Durchgang (ein Pass) liest; damit kann man ein komplettes Programm auf den Ruby-Standard-Input-Kanal umleiten und es klappt prima.

Wir können diese Fähigkeit von Ruby nutzen, um Ruby-Code aus einem Editor heraus laufen zu lassen. In Emacs kann man zum Beispiel einen Bereich mit Ruby-Text selektieren und mit dem Kommando Meta-| an Ruby zur Ausführung weitergeben. Der Ruby-Interpreter nimmt diesen Bereich als Standard-Input und der Output geht in einen Puffer mit Namen ``*Shell Command Output*.'' Diese Fähigkeit konnten wir sehr gut beim Schreiben dieses Buches gebrauchen --- wir haben einfach mitten im Kapitel ein paar Zeilen Ruby-Code selektiert und sie ausprobiert!

Man kann etwas ähnliches im vi-Editor machen mit ``:!ruby'', das ersetzt den Progamm-Text durch seinen Output, oder mit ``:w[visible space]!ruby'', das zeigt den Output an, ohne den Puffer zu belasten. Andere Editoren besitzen ähnliche Möglichkeiten.

Wo wir gerade dabei sind, sollten wir an dieser Stelle noch erwähnen, dass es da einen Ruby-Mode für den Emacs gibt, als misc/ruby-mode.el in der Distribution. Es gibt auch im Internet verschiedene Syntax-Highlighting-Module für vim (eine verbesserte Version von vi), jed und andere Editoren. Bei Ruby FAQ findet man die aktuellen Orte und ihre Verfügbarkeit.

Es klappt aber nicht!

Du hast also schon genügend von diesem Buch gelesen, du schreibst eigene Ruby-Progrramme und es klappt nicht. Hier kommt eine Liste mit typischen Fehlern und Tipps.

Es gibt eine wichtige Technik, die das Schreiben von Ruby-Code sowohl einfacher als auch erfreulicher macht. Entwickle deine Applikationen inkrementell. Schreibe ein paar Zeilen, dann lass sie laufen. Schreibe ein paar weitere, dann prüfe diese. Einer der großen Vorteile einer nicht typisierten Sprache ist es, dass die Sachen, die man benutzt, nicht fertig sein müssen.

Aber es ist zu langsam!

Ruby ist eine interpretierte, hoch-levelige Sprache, und als solche kann sie nicht so schnell sein wie eine niedrig-levelige Sprache wie C. In diesem Abschnitt zeigen wir einige der grundlegenden Sachen, die man machen kann, um die Performance zu erhöhen; siehe aber auch im Index unter Performance nach.

Erzeuge lokale Variablen außerhalb von Blöcken

Versuche, die in einem Block benutzten Variablen zu definieren, bevor der Block ausgeführt wird. Wenn du über eine sehr große Zahl von Elementen iterierst, kannst du die Ausführungsgeschwindigkeit etwas erhöhen, wenn du den Iterator vorher schon deklariert hast. In dem unten stehenden Beispiel muss Ruby für jede Iteration neue x- und y-Variablen erzeugen, in der zweiten Variante muss es das nicht. Wir benutzen das benchmark-Paket aus dem Ruby-Applikations-Archiv, um die Schleifen zu vergleichen:

require "benchmark"
include Benchmark
n = 1000000
bm(12) do |test|
  test.report("normal:")    do
    n.times do |x|
      y = x + 1
    end
  end
  test.report("predefine:") do
    x = y = 0
    n.times do |x|
      y = x + 1
    end
  end
end
erzeugt:
                  user     system      total        real
normal:       2.510000   0.000000   2.510000 (  2.390549)
predefine:    2.220000   0.010000   2.230000 (  2.157877)

Benutze den Profiler

Mit Ruby mitgeliefert gibt es einen Code-Profiler (der wird ab Seite 458 dokumentiert). An und für sich ist das keine Überraschung, aber wenn man sich vor Augen hält, dass dieser Profiler mit nur 50 Zeilen Ruby-Code geschrieben wurde, zeigt das die Stärken dieser Sprache.

Man kann das Profiling mit der Kommando-Zeilen-Option -r profile zu seinem Code hinzufügen oder von innerhalb mit require "profile". Ein Beispiel:

require "profile"
class Peter
  def initialize(amt)
    @value = amt
  end

  def rob(amt)     @value -= amt     amt   end end

class Paul   def initialize     @value = 0   end

  def pay(amt)     @value += amt     amt   end end

peter = Peter.new(1000) paul = Paul.new 1000.times do   paul.pay(peter.rob(10)) end

Man erhält dann ungefähr Folgendes:

 time   seconds   seconds    calls  ms/call  ms/call  name
 40.22     0.37      0.37        1   370.00   920.00  Fixnum#times
 25.00     0.60      0.23     1000     0.23     0.25  Paul#pay
 25.00     0.83      0.23     1000     0.23     0.30  Peter#rob
  7.61     0.90      0.07     1000     0.07     0.07  Fixnum#-
  2.17     0.92      0.02     1000     0.02     0.02  Fixnum#+
  0.00     0.92      0.00        1     0.00     0.00  Peter#initialize
  0.00     0.92      0.00        2     0.00     0.00  Class#inherited
  0.00     0.92      0.00        1     0.00     0.00  Paul#initialize
  0.00     0.92      0.00        4     0.00     0.00  Module#method_added
  0.00     0.92      0.00        2     0.00     0.00  Class#new
  0.00     0.92      0.00        1     0.00   920.00  #toplevel
Mit dem Profiler kann man sehr schnell Engpässe erkennen und beseitigen. Man sollte allerdings den Code hinterher ohne Profiler nochmal testen --- manchmal verdeckt die vom Profiler selbst erzeugte Verlangsamung andere Probleme.

Ruby ist eine wunderbar transparente und ausdrucksstarke Sprache, aber es befreit den Programmierer nicht davon, den gesunden Menschenverstand walten zu lassen: das Erzeugen unnötiger Objekte, das Ausführen von nicht gebrauchtem Code und das Erzeugen von aufgeblähtem Code sind in jeder Sprache überflüssig.
Debugger-Kommandos
b[reak] [file:]line Setze einen Breakpoint in der durch line angegebenen Zeile in Datei file (Default: aktuelle Datei).
b[reak] [file:]name Setze einen Breakpoint auf Methode method in file.
b[reak] Zeige die Breakpoints und Watchpoints (Überwachungspunkte) an.
wat[ch] expr Break, wenn der Ausdruck true wird.
del[ete] [nnn] Entferne Breakpoint nnn (Default: alle).
disp[lay] expr Zeige den Inhalt von nnn an, jedesmal wenn der Debugger die Kontrolle übernimmt.
disp[lay] Zeige die aktuellen Anzeigen.
undisp[lay] [nnn] Entferne diese Anzeige (Default: alle).
c[ont] Ausführung weiter laufen lassen.
s[tep] nnn=1 Die nächsten nnn Zeilen ausführen, dabei in Methoden hineinspringen.
n[ext] nnn=1 Die nächsten nnn Zeilen ausführen, dabei über Methoden hinwegspringen.
fi[nish] Ausführung der aktuellen Funktion beenden.
q[uit] Debugger verlasen.
w[here] Aktuellen Stack anzeigen.
f[rame] dasselbe wie where.
l[ist] [start--end] Die Zeilen von start bis end des Quellcodes anzeigen.
up nnn=1 nnn Ebenen im Stack hinaufgehen.
down nnn=1 nnn Ebenen im Stack hinab gehen.
v[ar] g[lobal] Globale Variablen anzeigen.
v[ar] l[ocal] Lokale Variablen anzeigen.
v[ar] i[stance] obj Instanz-Variablen von obj anzeigen.
v[ar] c[onst] Name Konstanten einer Klasse oder Modul-Namen anzeigen.
m[ethod] i[nstance] obj Instanz-Methoden von obj anzeigen.
m[ethod] Name Instanz-Methoden der Klasse oder Modul-Namen anzeigen.
th[read] l[ist] Alle Threads auflisten.
th[read] [c[ur[rent]]] Status des aktuellen Threads anzeigen.
th[read] [c[ur[rent]]] nnn Mache Thread nnn zum aktuellen Thread und halte ihn an.
th[read] stop nnn Mach Thread nnn zum aktuellen Thread und halte ihn an.
th[read] resume nnn Thread nnn wiederaufnehmennnn.
[p] expr Berechne den Wert von expr im aktuellen Kontext. expr darf dabei Zuweisungen an Variablen und Methodenaufrufe enthalten.
empty Ein null-Kommando wiederholt das letzte Kommando.


Extracted from the book "Programming Ruby - The Pragmatic Programmer's Guide"
Übersetzung: Jürgen Katins
Für das englische Original:
© 2000 Addison Wesley Longman, Inc. Released under the terms of the Open Publication License V1.0. That reference is available for download.
Diese Lizenz sowie das Original vom Herbst 2001 bilden die Grundlage der Übersetzung
Es wird darauf hingewiesen, dass sich die Lizenz des englischen Originals inzwischen geändert hat.
Für die deutsche Übersetzung:
© 2002 Jürgen Katins
Der Copyright-Eigner stellt folgende Lizenzen zur Verfügung:
Nicht-freie Lizenz:
This material may be distributed only subject to the terms and conditions set forth in the Open Publication License, v1.0 or later (the latest version is presently available at http://www.opencontent.org/openpub/). Distribution of substantively modified versions of this document is prohibited without the explicit permission of the copyright holder. Distribution of the work or derivative of the work in any standard (paper) book form is prohibited unless prior permission is obtained from the copyright holder.
Freie Lizenz:
Permission is granted to copy, distribute and/or modify this document under the terms of the GNU Free Documentation License, Version 1.1 or any later version published by the Free Software Foundation; with no Invariant Sections, with no Front-Cover Texts, and with no Back-Cover Texts. A copy of the license is included in the section entitled "GNU Free Documentation License".