2

Given the following Ruby class

class Example
  PARENTS = [
    FATHER = :father,
    MOTHER = :mother
  ]
end

These work as expected

> Example::PARENTS
#=> [:father, :mother]
> Example::PARENTS[0]
#=> :father
> Example::PARENTS[1]
#=> :mother

However, why does this work?

> Example::FATHER
#=> :father
> Example::MOTHER
#=> :mother

In fact, why are there three constants in the Example class's scope?

> Example.constants
#=> [:MOTHER, :PARENTS, :FATHER]

To the point, that if I extend the class with an additional method:

class Example
  def self.whos_your_daddy
    FATHER
  end
end

It accesses the constant like normally.

> Example.whos_your_daddy
#=> :father

How is this behavior possible? By declaring the constants inside of an array, I would expect them to to be scoped inside the array. Please cite the relevant docs in your answer.

Edit: I suppose I'll clarify, the easiest way to answer this question is to explain two things:

First, what happens when the following code is executed:

PARENTS = [
  FATHER = :father,
  MOTHER = :mother
]

Second, does declare a constant anywhere tie it to the scope of the class it is being declared in? Why?

Amin Shah Gilani
  • 8,675
  • 5
  • 37
  • 79
  • 3
    The visibility of the constants is given by `Example.constants => [:MOTHER, :PARENTS, :FATHER]`, [This article](https://www.linuxtopia.org/online_books/programming_books/ruby_tutorial/The_Ruby_Language_Scope_of_Constants_and_Variables.html) provides a good explanation of the scope of constants. – Cary Swoveland Feb 05 '19 at 01:32
  • @CarySwoveland the confusion in the question is a combination of the syntax used to declare the `PARENTS` constant, and because of a weak grasp on the scoping concepts. I suppose I should clarify this. I'm actually aware of the answer to the question. However, as pointed out already in the question, we already know the constants are in the class's scope, the question is: why? – Amin Shah Gilani Feb 05 '19 at 02:19
  • 1
    Solution: Don't use weakly typed languages so such gotchas would be caught by the compiler before they ever crop up as a bug in production – Ali Feb 05 '19 at 06:30
  • Why do you think an array becomes the scope for a constant? Please cite the relevant docs that says that. – sawa Feb 05 '19 at 06:53
  • @sawa, please don't aggressively edit the question to the point where it's drastically different. – Amin Shah Gilani Feb 05 '19 at 07:16
  • 1
    @ClickUpvote: Your comment doesn't seem to make much sense. Ruby is strongly typed. The question is about scope, so strong/weak typing isn't relevant anyway. Did you mean "dynamic"? – Eric Duminil Feb 05 '19 at 07:51
  • 3
    _"By declaring the constants inside of an array, I would expect them to to be scoped inside the array. Please cite the relevant docs in your answer."_ – could you cite the relevant docs that lead to your assumption? ;-) – Stefan Feb 05 '19 at 08:47
  • @Stefan, haha, good catch — I can't. This question is one I hear when people see me use this kind of declaration in my service objects, and so I tried to reproduce the confusion as much as possible in the question. I'm sorry for the confusion. This Q&A post was finally created after [this question](https://www.toptal.com/ruby-on-rails/rails-service-objects-tutorial#comment-4322654659). – Amin Shah Gilani Feb 05 '19 at 14:19

6 Answers6

5

First, what happens when the following code is executed:

PARENTS = [
  FATHER = :father,
  MOTHER = :mother
]
  • PARENTS = ... attempts to set the constant PARENTS. But in order to do so, it has to evaluate the right-hand side of the assignment:
    • [...] attempts to create an array. But in order to do so, it has to evaluate its arguments:
      • FATHER = :father sets the constant FATHER to :father. The result of this assignment is :father which becomes the first argument.
      • MOTHER = :mother sets the constant MOTHER to :mother. The result of this assignment is :mother which becomes the second argument.

So in chronological order:

  1. The constant FATHER is set to :father
  2. The constant MOTHER is set to :mother
  3. An array with elements :father and :mother is created
  4. The constant PARENTS is set to that array

Your code is equivalent to:

FATHER = :father
MOTHER = :mother
PARENTS = [FATHER, MOTHER]  # or [:father, :mother]

By declaring the constants inside of an array, I would expect them to to be scoped inside the array. Please cite the relevant docs in your answer.

You can use Module.nesting to determine the current nesting, i.e. where a constant will be defined in: (more examples in the docs)

class Example
  p outer_nesting: Module.nesting
  PARENTS = [
    p(inner_nesting: Module.nesting)
  ]
end

Output:

{:outer_nesting=>[Example]}
{:inner_nesting=>[Example]}

As you can see, an array literal doesn't affect the current nesting. Constants in both locations will be defined under Example.

If you really wanted to declare the constants "inside" the array (i.e. inside the array's singleton class), you could do some like this:

class Example
  PARENTS = []
  class << PARENTS
    FATHER = :father
    MOTHER = :mother
    PARENTS.push(FATHER, MOTHER)
  end
end

p Example.constants                          #=> [:PARENTS]
p Example::PARENTS.singleton_class.constants #=> [:FATHER, :MOTHER]

The above is just for demonstration purposes, there's no need to actually do that.

Stefan
  • 109,145
  • 14
  • 143
  • 218
  • I came here to modify my answer with the exact same code as yours. Do you know how to reference `PARENTS` inside the singleton class without having to actually write `PARENTS`? `super` doesn't work outside of a method definition. – Eric Duminil Feb 05 '19 at 09:07
  • 1
    @EricDuminil i don't think you can retrieve an object from its singleton class, although that would be an interesting addition. – Stefan Feb 05 '19 at 09:23
  • 1
    Let's [see](https://stackoverflow.com/questions/54531270/retrieve-a-ruby-object-from-its-singleton-class)! – Eric Duminil Feb 05 '19 at 09:33
3

I can understand how it can be confusing, but apart from the fact that reassigning values to constants is discouraged, in terms of scope — instance variables and constants are extremely similar.

The visual trick in the first class declaration is where you declare the constant inside an array.

First: understand that when you declare a constant, the returned value is your definition. E.g.

FATHER = :father
#=> :father

Now, let's look at the constant declaration:

PARENTS = [
  FATHER = :father,
  MOTHER = :mother
]

For declaring PARENT, you could have just used:

PARENTS = [
  :father,
  :mother
]

but you went a step further and declared a constant inside the definition as well. Now please understand that the scope of instance variables and constants is similar, it is tied to the instance it was declared in, and so declaring a constant anywhere would tie it to the instance.

By executing FATHER = :father, you've declared another constant, and the scope of a constant is always going to be the class it was declared in, in this case Example. The same is also true for MOTHER = :mother.

If you're more used to instance variables — it's the same reason this works: Given the following Ruby class.

class Example
  @parents = [
    @father = :father,
    @mother = :mother
  ]
end

These work as expected

> Example.instance_variable_get :@parents
#=> [:father, :mother]
> Example.instance_variable_get(:@parents)[0]
#=> :father
> Example.instance_variable_get(:@parents)[1]
#=> :mother

But this works too.

> Example.instance_variable_get :@father
#=> :father
> Example.instance_variable_get :@mother
#=> :mother

In fact, these three are in the Example class's scope.

> Example.instance_variables
#=> [:@mother, :@parents, :@father]

To the point, that if I extend the class with an additional method:

class Example
  def self.whos_your_daddy
    @father
  end
end

It accesses the instance variables like normally.

> Example.whos_your_daddy
#=> :father
Amin Shah Gilani
  • 8,675
  • 5
  • 37
  • 79
2

I found this in the ruby ​​programming book, page 94:

Constants defined within a class or module may be accessed unadorned anywhere within the class or module. Outside the class or module, they may be accessed using the scope operator, ::, prefixed by an expression that returns the appropriate class or module object. Constants defined outside any class or module may be accessed unadorned or by using the scope operator with no prefix. Constants may not be defined in methods. Constants may be added to existing classes and modules from the outside by using the class or module name and the scope operator before the constant name.

Conclusion, there cannot be two constants with the same name within a class, one inside the array and one outside. So you do not need the scope to access it, since the scope is the whole class.

You will not need a constant within a constant array, since the array being constant its internal values ​​are constant too.

sawa
  • 165,429
  • 45
  • 277
  • 381
  • 1
    Thank you for taking the time to answer my question as your first one on the site, and welcome to Stack Overflow! :) There's a small bit of confusion in your answer — "there can not be two CONSTANTS with the same name". At no point did I use two constants with the same name. – Amin Shah Gilani Feb 05 '19 at 02:13
  • 1
    Please provide the source as well. Which book was this? What version? – Joseph Cho Feb 05 '19 at 03:00
2

A short answer could be that:

Your code is equivalent to:

class Example
  PARENTS = [
    Example.const_set("FATHER", :father),
    Example.const_set("MOTHER", :mother)
  ] 
end

with some tests:

puts Example.is_a? Module
# true
puts Example.is_a? Class
# true
p Example::PARENTS
# [:father, :mother]
p Example.constants
# [:FATHER, :MOTHER, :PARENTS]
puts Example::PARENTS.is_a? Module
# false
Example::PARENTS.const_set("UNCLE", :uncle)
# undefined method `const_set' for [:father, :mother]:Array (NoMethodError)

*: Top-level constants (= constants defined outside a class or module) seem to be stored in Object.

Eric Duminil
  • 52,989
  • 9
  • 71
  • 124
1

Your answer is in your question.

class Example
  PARENTS = [
    FATHER = :father,
    MOTHER = :mother
  ]
end

Here 3 constants (PARENTS, FATHER and MOTHER). And they in one scope. Array doesn't make new scope.

And Example.constants method just shows you them.

Even if you add your method to your class it changes absolutely nothing

class Example
  PARENTS = [
    FATHER = :father,
    MOTHER = :mother
  ]

  def self.whos_your_daddy
    FATHER
  end
end

Example.constants #=> [:MOTHER, :PARENTS, :FATHER]
mechnicov
  • 12,025
  • 4
  • 33
  • 56
  • 1
    Your entire answer is almost just repetition of the OP's question, and adds no value. The only part that is useful is "Array doesn't make new scope". – sawa Feb 05 '19 at 06:58
0

Array definition is not a block. It does not run in Array's scope.

It's no different from:

PARENTS = []
PARENTS << mom = :mother

As you can see, scope does not change:

> puts self
main
=> nil
> array = [ puts(self.inspect) ]
main
=> [nil]

Assignment returns the assigned value:

> foo = "bar"
"bar"
> puts foo
bar
> puts(foo = "baz")
baz
> puts foo
baz

You can't "store" something like MOTHER = :mother in an Array as it is not a value, it returns a value, which is :mother.

Kimmo Lehto
  • 5,910
  • 1
  • 23
  • 32