I am quite unable to resist.
Yesterday, I found a workaround for a problem, but more via guesswork than actual understanding. I hate it when that happens.
The problem occurred when I tried to access a class variable from helper methods mixed into the Sinatra app. The actual error was:
uninitialized class variable @@db in Eee::HelpersThe class variable
@@db
was defined and Eee::Helpers
was the module namespace being mixed into the Sintra application.Rather than try to work through the Sinatra code (that's some hearty meta-programming!), I try to break the problem down to the simplest possible form.
The simplest possible class that allows class variable access is something along the lines of:
class FooTrying this out in an interactive ruby session,
@@foo = :foo
def self.foo
@@foo
end
end
Foo.foo
returns the expected value, the symbol :foo
:>> Foo.fooInstead of defining the
=> :foo
foo
class method as above, it is also possible to do it thusly:class FooExecuting this in
@@foo = :foo
class << self
def foo
@@foo
end
end
end
irb
, I get the exact same results:>> Foo.fooOne might be tempted to think of the above two forms as identical ways to define class methods, but that is not quite accurate. The first way (
=> :foo
def self.foo
) creates what one would normally think of as a class method.The second way opens the class's eigenclass to define the method. Eigenclasses are more useful with instances than with classes. Defining a method on an instance's eigenclass allows you to define a method on a single instance of a class rather than all instances. When trying to resolve where an object's method is defined, Ruby will first check the object's eigenclass before checking "normal" methods.
Since Ruby classes are, themselves, objects, it is possible to use eigenclasses with them as shown above. All that is just fine, but where was I going wrong yesterday? Both earlier examples work just fine.
The trouble occurs when I define the method outside of the class:
class FooWhen I try to access
@@foo = :foo
end
class << Foo
def foo
@@foo
end
end
Foo.foo
in irb
, I get:>> Foo.fooA singleton method is another name for an eigenclass class method. The access to the class variable is considered top level because the class keyword does not define a class name that would provide a scope to hold the class variable.
(irb):6: warning: class variable access from toplevel singleton method
NameError: uninitialized class variable @@foo in Object
from (irb):6:in `foo'
from (irb):9
That is still not quite the error that I got yesterday—I did not get the warning about the toplevel singleton object, just the warning about the uninitialized class variable. The reason this was not part of my failure chain yesterday was because I had mixed in the class method:
class FooNow, when I try to access
@@foo = :foo
end
module Bar
def foo
@@foo
end
end
class << Foo
include Bar
end
Foo.foo
, I get the same error that was getting yesterday:>> Foo.fooAnd just like yesterday, I can resolve this by by-passing the eigenclass with a direct
NameError: uninitialized class variable @@foo in Bar
from (irb):6:in `foo'
from (irb):12
class_variable_get
call:class FooNow, when I call
@@foo = :foo
end
module Bar
def foo
self.send(:class_variable_get, :@@foo)
end
end
class << Foo
include Bar
end
Foo.foo
, I get the desired symbol :foo
from the class variable—even though I get it through the toplevel singleton / eigenclass:>> Foo.fooSo, it turns out my educated guess from yesterday was actually right. Nine times out of ten, my educated guesses are wildly wrong. It is nice to know that I got this one right—it gives me confidence moving forward that I am not programming by coincidence.
=> :foo
Great post. Just catching up on some of your prolific posts! :)
ReplyDelete