Beim basteln bin ich wieder über einen kleinen, aber feinen Unterschied zwischen Ruby 1.9 und Ruby 1.8 gestoßen, der eventuell verwunderlich sein kann: der Lookup von Konstanten wurde geändert. Meistens wird das nicht auffallen, allerdings gibt es einige Fälle, die einen erstmal etwas ratlos zurück lassen...
Bildquelle: http://www.flickr.com/photos/piulet/ / CC BY-NC-ND 2.0
Zuerst einmal ein kleines bisschen Beispielcode:
1 2 3 4 5 6 7 8 9 10 |
class Foo; end class Bar Foo.class_eval do class Batz; end end end puts "Konstanten in Foo: #{Foo.constants.inspect}" puts "Konstanten in Bar: #{Bar.constants.inspect}" |
Führen wir diesen Code nun aus, erhalten wir folgendes:
$ ruby constants.rb Konstanten in Foo: [] Konstanten in Bar: ["Batz"] $ ruby19 constants.rb Konstanten in Foo: [:Batz] Konstanten in Bar: []
Ärgerlich. Und hier liegt der Hund begraben: in Ruby 1.8 ist der Lookup von Konstanten lexikalisch. Das heisst, dass der Parser den Kontext vorgibt, in dem der Lookup stattfindet. In diesem Fall befinden wir uns im lexikalischen Kontext der Klasse Bar, also wird die neue Konstante Batz der Konstantenliste von Bar hinzugefügt, also zu Bar::Batz. Dass zwischendurch der Ausführungskontext gewechselt wird (durch einen Aufruf von #class_eval) interessiert hier nicht. Anders in Ruby 1.9: hier geschieht der Lookup immer dynamisch mittels self. Das hat die Konsequenz, dass Batz an Foo angehängt und damit zu Foo::Batz wird.
Das hat mehrere Konsequenzen. Zum ersten wird solcher Code ohne Hacks möglich (im übrigen der Fall, über den ich drauf gekommen bin):
1 2 3 4 5 |
def self.load_in_module(file, mod) mod.class_eval do eval(File.read file, binding, file, 0) end end |
Folgender Code wird aber nicht mehr so einfach funktionieren (etwas weit hergeholt und enthält einen fiesen Bug, dafür kurz):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
require 'ostruct' module Configurable class Config < OpenStruct def method_missing(sym, *args) unless sym =~ /=$/ super :"#{sym}=", *args end end end def config(&block) @config ||= Config.new @config.instance_eval &block end end class Sample extend Configurable class MyExceptionHandler end config do exception_handler MyExceptionHandler end end |
Dieser Code funktioniert ohne Problem in Ruby 1.8, macht dann aber in 1.9 Probleme:
uninitialized constant Configurable::Config::MyExceptionHandler from constants.rb:26:in `instance_eval' from constants.rb:26:in `config' from constants.rb:38:in `class:Sample' from constants.rb:32:in `main'
Klar, innerhalb von #config ist self nun die Instanz von Configurable::Config, die die Konstante Sample::MyExceptionHandler nicht kennt.
Ein paar Techniken, dieses Problem zu umgehen, erklärt Coderr auf seinem Blog: Dynamically adding a Constant Nesting Fixing Constant Lookup.
Mir persönlich gefällt das neue, programmatischere Verhalten besser, viele verfluchen es. Wer will, kann sich mit Yehuda Katz stundenlang darüber streiten.
Ein kleines Schmankerl zuletzt. Folgendes funktioniert sowohl in Ruby 1.8 als auch 1.9:
1 2 3 4 5 6 7 8 |
module Foo class Bar end end x = Foo puts x::Bar |
Eine sehr elegante Variante, Komponenten gleicher Form zu verpacken. Vor allem, da in Ruby 1.9 nun Folgendes eine Trivialität ist, in 1.8 fast unmöglich:
1 2 3 |
with_module x do Bar.new end |
Kommentar schreiben
Kommentare
class A; end; class B; class ::A; Foo = 5; end; end; puts A::Foo #=> 5
Ich finde das Verhalten sehr konsequent. Entweder verpacke ich meine Konstante in einen umliegenden Scope, oder die Konstante wird dynamisch gesucht.
Das funktioniert aber sowohl mit einem lexikalischen als auch mit einem dynamischen Scope.
Soweit ich das verstanden habe, sollten Konstanten nun mehr funktionieren wie Klassen- und Instanzvariablen (Empfängerbasiert/dynamisch), anstatt wie lokale Variablen (lexikalisch/statisch).
Dass dein Beispiel nicht mehr funktioniert, ist allerdings ärgerlich. Die vorgeschlagenen Fixes von Coderrr sind technisch faszinierend, aber gruselig. Passt also super hinein in ActiveSupport.
Naja, des einen Freud’, des anderen Leid. Wie gesagt, das with_module-Beispiel und mein Anwendungsfall mit eval waren vorher so gut wie unmöglich. Wenn man die Fragen dazu auf ruby-lang sieht, scheint das neue Verhalten sogar etwas natürlicher zu sein. Klar, so mancher DSL-Entwickler wird sich jetzt genervt fühlen, aber sie werden drumherum kommen.