Die Entwicklung von Ruby 2.0 ist die konsequente Weiterentwicklung der eher experimentellen 1.9er-Versionen. Auf dem Weg von Ruby 1.9 zum neuen Ruby wurden knapp 800 Bugs behoben und über 320 kleine und große Features hinzugefügt.

Zu den Neuerungen, die die Ruby-Programmierer am meisten beeinflussen werden, gehören sicherlich:

Die folgenden Beispiele wurden mit der Ruby-Version 2.0.0dev (2013–02–08 trunk 39161) getestet.

Besseres Flickwerk

Bei der RubyConf 2010 hatte Shugo Maeda zum ersten Mal das Konzept sogenannter Refinements in Ruby vorgestellt [1]. Ähnliche Konzepte sind in anderen Programmiersprachen unter den Namen “Selector namespaces” oder “Classboxes” bekannt. Die Grundidee hinter den Refinements ist es, das sogenannte Monkey-Patching sicherer zu machen, und zwar in dem Sinn, dass Klassenveränderungen zum Beispiel von Ruby-Core-Klassen durch Refinements nur in einem bestimmten Kontext Auswirkungen haben und nicht “global”. Der Entwickler kann weiterhin Klassen zur Laufzeit verändern, verringert aber durch die Refinements das Risiko, dass sich Klassenerweiterungen verschiedener Bibliotheken negativ beeinflussen. Das ist sicherlich ein Problem des klassischen Monkey-Patchings und vor allem im Rails-Kontext anzutreffen, wo viele Konstrukte über Monkey-Patching realisiert werden [2].

Um ein Refinement zu erstellen, ist die Methode refine innerhalb eines Moduls zu verwenden. In diesem Fall ist refine kein Ruby-Schlüsselwort. Die Methoden-Signatur sieht folgendermaßen aus:

Module#refine(klass, &block)

Die Methode erwartet die zu erweiternde Klasse und einen Block. Module lassen sich nach derzeitigem Stand nicht übergeben. Im Block werden dann die eigentlichen Erweiterungen definiert. Laut Spezifikation kann der Entwickler mehrere refine-Statements in einem Modul verwenden.

# my_active_support.rb module MyActiveSupport refine String do def blank? self !~ /[^[:space:]]/ end end refine NilClass do def blank? true end end refine FalseClass do def blank? true end end refine TrueClass do def blank? false end end end

Um die erstellten Refinements nutzen zu könnnen, sind sie jetzt mit dem Aufruf using zu aktivieren, das sich als Soft-Keyword ansehen lässt.

# main.rb require 'my_active_support.rb'

using MyActiveSupport

' '.blank? #=> true

‘Hello’.blank? #=> false

nil.blank? #=> true

Im obigen Beispiel sind die Refinements von MyActiveSupport ab dem Aufruf von using bis zum Ende der main.rb-Datei gültig. Es lässt sich daher sagen, dass Refinements eine Art lexikalischen Anwendungsbereich (Scope) besitzen. Das nachfolgende Beispiel soll das Ganze noch etwas verdeutlichen.

# main.rb module MyActiveSupport refine Array do def sum inject(:+) end end end

def call_sum(ary) ary.sum end

using MyActiveSupport

ary = [5,5] p ary.sum #=> 10

p call_sum(ary) #=> undefined method ‘sum’

Am Beispiel erkennt der Leser gut die Auswirkungen eines Methodenaufrufs außerhalb des Refinement-Anwendungsbereichs. Da die Methode call_sum vor using definiert wird, steht die erweiterte Array-Methode sum zum Zeitpunkt des Aufrufs nicht zur Verfügung. Das löst anschließend einen Laufzeitfehler aus [3].

Dass dieses Feature als experimentell gekennzeichnet wurde, obgleich es auf den ersten Blick ein konsistentes Verhalten zu haben scheint, hat “Matz” mehr oder weniger selbst beantwortet [4]:

Since there still remain undefined corner case behavior in refinements, and the time is running out, I decided not to introduce full refinement for Ruby 2.0.

Er bezieht sich dabei auf Punkte wie Performance oder auch die bislang unklare Vererbung von Refinements. Der JRuby-Entwickler Charles Nutter hat in seinem Blog [5] eine etwas ältere Spezifikation der Refinements ebenfalls kritisch beäugt. Es bleibt also abzuwarten, in welcher Form die Refinements in späteren Ruby-Versionen vorliegen. Jeder sollte frühzeitig überlegen, ob er das Feature jetzt schon einsetzen möchte.

Faulheit als Plus

Die “faule” Auswertung von Listen ist eine Erweiterung, die vor allem Programmierer mit Kenntnissen in funktionalen Programmiersprachen freuen wird. Sie ermöglicht eine äußerst kompakte Schreibweise für das Abarbeiten von Schleifen und (unendlichen) Collections, da temporäre Arrays nicht mehr nötig sind. Beispielsweise lässt sich:

require 'prime' a = [] Prime.each do |x| next if x % 4 != 3 a << x break if a.size == 10 end

durch

Prime.lazy.select {|x| x % 4 == 3 }.take(10).to_a

ersetzen. Das richtig Interessante neben der Schreibweise ist aber die Tatsache, dass Entwickler, wie im zweiten Fall, die Enumeratoren verketten können. Die Auswertung geschieht dann nicht mehr direkt, sondern ein “lazy” Enumerator liefert wiederum einen weiterverwendbaren Enumerator::Lazy zurück. Erst wenn man tatsächlich einen Wert zurückbekommt, wird der Ausdruck ausgewertet. Den Vorteil verdeutlicht die Betrachtung folgender Tests:

require 'test/unit' include Test::Unit::Assertions a = [1,2,3,4,5,6]

Ohne lazy wird das Array komplett abgearbeitet.

assert_equal(4, a.select {|x| x > 3}.first)

Mit lazy stoppt die Abarbeitung sobald der erste Wert grösser drei gefunden wurde

assert_equal(4, a.lazy.select {|x| x > 3}.first)

Für die oft verwendeten Methoden map, flat_map, select, reject, grep, zip, take, take_while, drop, drop_while und cycle gibt es ebenfalls eine Enumerator::Lazy-Implementierung.

Trotz der Vorteile durch kompakte Schreibweise, Verkettung und Abarbeiten unendlicher Collections darf eines nicht unerwähnt bleiben: die Abarbeitungsgeschwindigkeit kann sich ungefähr um den Faktor 4 verlangsamen. Der Grund ist, dass während der Verkettung von Enumeratoren die Teilergebnisse im Hauptspeicher verbleiben müssen, da sie als Parameter in der weiteren Abarbeitung verwendet werden.

Wider den Hack

In Ruby 2.0 findet man ein neues, in einigen anderen Programmiersprachen ebenfalls bekanntes Feature: die auch “named parameters” genannten “keyword arguments”. Bislang hat man in Ruby über eine Notlösung ein ähnliches Verhalten implementieren können, indem ein Hash beim Methoden-Aufruf übergeben wurde. Wollte der Entwickler Default-Werte verwenden, wurden diese meist über merge() mit den eigentlichen Argumenten zusammengeführt.

def config(opts={}) default_values = {ssl: true, timeout: 500} opts = default_values.merge(opts) end

config #=> {:ssl=>true, :timeout=>500}

config :ssl => false, :timeout => 200 #=> {:ssl=>false, :timeout=>200}

Auf diesen “Hack” lässt sich dank des neuen Features nun verzichten. In einem Methodenaufruf kann der Entwickler für die Parameter direkt Namen und in der Methoden-Deklaration für diese Argumente dementsprechend auch Default-Werte angeben. Die nachfolgenden Beispiele sollen die Möglichkeiten verdeutlichen.

def phrase(question: 'The answer to life the universe and everything', answer: '42') [question, answer] end

phrase #=> ["The answer to life the universe and everything", "42"]

phrase question: ‘The answer’ #=> ["The answer", "42"]

phrase question: ‘The answer’, wrong_answer: ‘43’ #=> ArgumentError: unknown keyword: wrong_answer

Der Entwickler kann auch optionale Parameter angeben, die direkt als Hash- Wert interpretiert werden. Dafür schreibt er vor den eigentlichen Parameter ein _**_. Das wandelt die optionalen Parameter direkt in einen Hash-Wert um, was praktisch sein kann.

def config(ssl: true, timeout: 500, **options) [ssl, timeout, options] end

config #=> [true, 500, {}]

config ssl: false, timeout: 100, test_mode: true #=> [false, 100, {:test_mode=>true}]

Zum Schluss noch ein etwas kryptischeres Beispiel in Verbindung mit dem Splat-Operator.

def config(*clients, ssl: true, timeout: 500, **options) [clients, ssl, timeout, options] end

config #=> [[], true, 500, {}]

config ‘foo’, ‘bar’, ssl: false, timeout: 100, test_mode: true, max_response: 50 #=> [["foo", "bar"], false, 100, {:test_mode=>true, :max_response=>50}]

Weitere Möglichkeiten findet man in den Testfällen [6] für die “keyword arguments”.

Dies und das

Einige kleinere, aber nicht unwesentlichere, neue Features, seien am Schluss kurz und kompakt vorgestellt.

Module#prepend

Laut Core-Developer Yusuke Endoh wird Module#prepend wahrscheinlich eines der zumeist unbewusst genutzten Features werden. Es ersetzt die alias_method_chain in Rails mit einem eindeutigeren Mechanismus unter der Verwendung von Modulen. Derzeit werden Module mit der Anweisung include in einer Klasse benutzt und stellen ihre Methoden zur Verfügung. Methoden der Klasse dürfen Modulmethoden einfach überschreiben, sofern diese per super erreichbar sind. Umgekehrt ist das im Moment noch nicht möglich. An der Stelle kommt die Anweisung prepend ins Spiel. Werden Module über diese Module einer Klasse vorangestellt, überschreiben die Methoden des Moduls die der Klasse, sofern die Klassenmethoden per super erreichbar sind.

RubyGems 2.0

Zusammen mit dem neuen Ruby wird RubyGems 2.0 ausgeliefert [7], das einige Verbesserungen des Ruby-Paketsystems mitbringt. Besonders erwähnenswert ist die Unterstützung sogenannter gemdeps-Dateien. Sie enthalten Informationen darüber, welche Gems mit welcher Version für das Ausführen der Anwendung benötigt werden. Der Funktionsumfang ist zwar noch nicht ganz Bundler-konform, bietet aber trotzdem eine einfache Benutzung dieser Dateien. Beispielsweise kann ein gem install aus dieser Datei die benötigten Gems installieren.

Onigmo als Engine für reguläre Ausdrücke

Bereits mit Ruby 1.9. wurde Onigmo [8] als Engine für reguläre Ausdrücke eingeführt, mit Ruby 2.0 ist sie nun die erste Wahl. Onigmo ist ein Fork von Oniguruma [9], einer an Perl-Syntax angelehnten RegEx-Bibliothek. In die aktuelle Version von Onigmo sind unter anderem Fixes von Ruby 1.9.3 und Perl 5.10+ eingeflossen. Diese Engine verspricht einige neue Möglichkeiten, zum Beispiel bedingte Auswertungen und Nutzung von Rückverweisen.

Gabriele Renzi fasst in seinem Blog die Features von Onigmo in Ruby sehr gut zusammen und präsentiert auch einige Beispiele [10].

Upgrade von Ruby 1.9 auf Ruby 2.0

Im Moment gibt es eine überschaubare Anzahl an Inkompatibilitäten:

Fazit

Das Upgrade auf Ruby 2.0 für bestehende Programme sollte ohne größere Probleme möglich sein, zumindest laut Yusuke Endoh [11]. Es wurde Wert darauf gelegt, dass das neue Ruby nahezu komplett kompatibel zu Ruby 1.9 ist. Da zudem die Gesamtperformance in Ruby 2.0 verbessert wurde, ist ein Upgrade lohnenswert. Der Wunsch des Core-Teams ist es, dass Ruby 1.9.3 so schnell wie möglich abgelöst werden kann.

Zum Schluss bleibt noch die Frage offen, wie das populäre Webframework Ruby on Rails mit einer neuen Ruby-Version umgeht? Wenig überraschend ist die Antwort, dass die zukünftige Version von Ruby on Rails (4.0), die mindestens Ruby 1.9.3 verlangt, den Einsatz von Ruby 2.0 empfiehlt [12].

May Ruby be with you!

Referenzen

  1. http://www.slideshare.net/ShugoMaeda/rc2010-refinements  ↩

  2. http://yehudakatz.com/2010/11/30/ruby-2-0-refinements-in-practice/  ↩

  3. https://bugs.ruby-lang.org/projects/ruby-trunk/wiki/RefinementsSpec  ↩

  4. https://bugs.ruby-lang.org/issues/4085#note-175  ↩

  5. http://blog.headius.com/2012/11/refining-ruby.html  ↩

  6. https://github.com/ruby/ruby/blob/trunk/test/ruby/test_keyword.rb  ↩

  7. http://blog.rubygems.org/2012/12/03/2.0.0-preview2.html  ↩

  8. https://github.com/k-takata/Onigmo  ↩

  9. http://www.geocities.jp/kosako3/oniguruma/  ↩

  10. http://www.riffraff.info/2012/2/22/matching-nested-structures-with-regexps-in-ruby-1-9  ↩

  11. http://www.infoq.com/news/2012/11/ruby-20-preview1  ↩

  12. http://edgeguides.rubyonrails.org/4_0_release_notes.html  ↩