ruby-mine

exploring the mine

Die meistgehasste Änderung

von skade am 07.10.2009 (15 Uhr)

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

Name (notwendig)

Mail (wird nicht veröffentlicht)

Webseite


Kommentare

  1. Kai schrieb am 07.10.2009 (21 Uhr)

    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.

  2. Skade schrieb am 08.10.2009 (01 Uhr)

    Das funktioniert aber sowohl mit einem lexikalischen als auch mit einem dynamischen Scope.

  3. murphy schrieb am 11.10.2009 (00 Uhr)

    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.

  4. Skade schrieb am 11.10.2009 (18 Uhr)

    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.