Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Ruby: defining constants using class_eval can be found only via const_get but not via directly :: lookup

Given a User class:

class User
end

I want to define a new constant using .class_eval. So:

User.class_eval { AVOCADO = 'fruit' }
  1. If I try to access it via User::AVOCADO, I get uninitialized constant User::AVOCADO, but User.const_get(:AVOCADO) works. Why?

  2. If I define a constant inside a Rails Concern in the included method and include the concern in the User class, I can access it via the regular :: lookup. For instance:

module FruitConcern
  extend ActiveSupport::Concern

  included do
     AVOCADO = 'fruit'
  end

end

class User
  include FruitConcern
end

User::AVOCADO
=> 'fruit'

However, looking up the source code for ActiveSupport::Concern, included just stores that block in an instance variable (@_included_block), and then it's override of append_features will call base.class_eval(&@_included_block).

So, if it's just calling User.class_eval with the same block, why User::AVOCADO works when the constant is defined inside that included block, but not when I call User.class_eval { AVOCADO = 'fruit' } directly?

  1. Weirdly (see this blog post), when doing User.class_eval { AVOCADO = 'teste' }, it seems Ruby also leaks the constant to the top level. So:
User.const_get(:AVOCADO)
=> "fruit"
AVOCADO
=> 'fruit'

I know blocks have flat scopes, but the constant was NOT defined beforehand, and I expected class_eval to change both self and the default definee to the receiver. What is going on here? And how is this constant being defined twice, at the top-level and also in the User scope?

like image 417
sandre89 Avatar asked Sep 14 '25 16:09

sandre89


2 Answers

When you define constants you're not assigning a constant to self. You're defining a constant in the current module nesting.

When you explicitly open a class or module you're also setting the module nesting:

module Foo
  BAR = 1
  puts Module.nesting.inspect # [Foo]
end

When you do User.class_eval { AVOCADO = 'fruit' } the module nesting is "Main" aka the global object:

User.class_eval do
  ADVOCADO = 'Fruit'
  puts Module.nesting.inspect # []
end

Blocks do not actually change the module nesting. const_set on the other hand defines a constant inside of another module nesting.

Ruby also isn't actually defining the constant twice. Rather when you use const_get or reference a constant without explitly using the scope resolution operator Ruby will look through the module nesting and walk the tree up to the global scope:

class A
end
B = 'eureka'
A.const_get(:B) # 'eureka'

Thats how you can reference top level constants without prefacing them with ::. But when you use User::ADVOCADO you're explicitly referencing the constant inside of User.

When it comes to the example with the concern you have missunderstood whats happening. Is not at all about class_eval. You're defining the constant FruitConcern::AVOCADO.

When you then include FruitConcern into User you're adding FruitConcern onto the ancestors chain which is included in constant lookup:

module FruitConcern
  ADVOCADO = 'fruit'
end

class User
  include FruitConcern
end

User::ADVOCADO  # fruit

See:

like image 194
max Avatar answered Sep 17 '25 06:09

max


There are three implicit contexts in Ruby:

  • self, the "current object": the implicit receiver and the scope for instance variables.
  • The default definee: where methods end up that are defined with def bar without an explicit target (i.e. not def foo.bar).
  • Constant scope.

The first two are explained very well in the linked article. An article for the third one is promised in the linked article, but it never appeared. Many people have written many words about constants, but unfortunately, nobody has written an authoritative specification, similar to what yugui did for the default definee.

So, unfortunaly, I, too can only speculate, and thus unhelpfully add to the mountain of non-authoritative words written about this topic.

Blocks are lexically scoped in Ruby, and they capture their lexical environment. In general, that means that references inside the block mean exactly the same thing as outside the block, as if the block wasn't there. (The exception are of course block local variables.)

So,

foo { AVOCADO = 'fruit' }

means the same thing as

AVOCADO = 'fruit'

Unless, of course, foo somehow changes the context in which the block is evaluated. We know that instance_eval changes self. We know that class_eval changes self and the default definee. However, the important thing is: class_eval does not change the implicit constant scope.

Hence, the assignment AVOCADO = 'fruit' inside

User.class_eval { AVOCADO = 'fruit' }

has exactly the same meaning as it would have, if it were outside the block:

AVOCADO = 'fruit'

In other words, it has the same meaning as a constant assignment at the top-level, and as we know, at the top-level:

  • self is the unnamed singleton object, commonly called main,
  • the default definee is Object, with the added twist that methods become private by default, and
  • the implicit constant scope is also Object.

So, AVOCADO gets defined in Object.

Which means that the following works:

class User
  AVOCADO
end
#=> 'fruit'

Because constant lookup goes first lexically "outward" (where it fails in this case), then "upward" in the inheritance hierarchy, where it succeeds, because User implicitly is a subclass of Object.

User.const_get(:AVOCADO)
#=> 'fruit'

also works, because that's actually the same thing as the one before, just done dynamically with reflection: it starts the constant lookup algorithm in class User, same as if you had written

class User
  AVOCADO
end

However, this does not work:

User::AVOCADO

Which, to be honest, is confusing, because AVOCADO should be inherited from Object.

like image 27
Jörg W Mittag Avatar answered Sep 17 '25 07:09

Jörg W Mittag