Crystal: Unexpected behaveour of class variables in children classes

Created on 3 Nov 2019  路  10Comments  路  Source: crystal-lang/crystal

class A
  @@bla = [1, 2, 3]

  def self.bla
    @@bla
  end
end

class B < A
end

p A.bla
p B.bla

A.bla << 4

p A.bla
p B.bla
crystal 1.cr
[1, 2, 3]
[1, 2, 3]
[1, 2, 3, 4]
[1, 2, 3]
ruby 1.cr
[1, 2, 3]
[1, 2, 3]
[1, 2, 3, 4]
[1, 2, 3, 4]

Most helpful comment

Yes this is intentionally different from Ruby, whose behavior is the more surprising one generally and a common pitfall there.

All 10 comments

This behavior is documented here https://crystal-lang.org/reference/syntax_and_semantics/class_variables.html

Class variables are inherited by subclasses with this meaning: their type is the same, but each class has a different runtime value.

I mean the behavior of class variables is coherent with behavior of instance variables in Crystal:

class A
  @bla = [1, 2, 3]

  def bla
    @bla
  end
end

class B < A
end

a = A.new
b = B.new

p a.bla
p b.bla

a.bla << 4

p a.bla
p b.bla
crystal 1.cr
[1, 2, 3]
[1, 2, 3]
[1, 2, 3, 4]
[1, 2, 3]

Yes this is intentionally different from Ruby, whose behavior is the more surprising one generally and a common pitfall there.

Having different class variables for inherited classes have no sense. I think possibility of share @@ variables was the main goal why they was invented in ruby.

Having different class variables for inherited classes have no sense.

Yet having different instance variables for "inherited instances" has perfect sense?
Why?

Official Ruby FAQ warns about possible problems when using class variables in Ruby:

https://www.ruby-lang.org/en/documentation/faq/8/

What is the difference between class variables and class instance variables?

The main difference is the behavior concerning inheritance: class variables are shared between a class and all its subclasses, while class instance variables only belong to one specific class.

Class variables in some way can be seen as global variables within the context of an inheritance hierarchy, with all the problems that come with global variables. For instance, a class variable might (accidentally) be reassigned by any of its subclasses, affecting all other classes:

class Woof

  @@sound = "woof"

  def self.sound
    @@sound
  end
end

Woof.sound  # => "woof"

class LoudWoof < Woof
  @@sound = "WOOF"
end

LoudWoof.sound  # => "WOOF"
Woof.sound      # => "WOOF" (!)

Or, an ancestor class might later be reopened and changed, with possibly surprising effects:

class Foo

  @@var = "foo"

  def self.var
    @@var
  end
end

Foo.var  # => "foo" (as expected)

class Object
  @@var = "object"
end

Foo.var  # => "object" (!)

So, unless you exactly know what you are doing and explicitly need this kind of behavior, you better should use class instance variables.

@asterite Sorry to ping you, but was the following behavior also intentional?

UPDATE:
My statement below is not true. See equivalent Crystal code by @straight-shoota below https://github.com/crystal-lang/crystal/issues/8427#issuecomment-549178421

On the interesting side note class variables in Crystal ARE NOT semantically equivalent to class instance variables in Ruby either. E.g. each class instance does not seem to have a separate variable like in this Ruby example from Official Ruby FAQ:

class Entity

  @instances = 0

  class << self
    attr_accessor :instances  # provide class methods for reading/writing
  end

  def initialize
    self.class.instances += 1
    @number = self.class.instances
  end

  def who_am_i
   "I'm #{@number} of #{self.class.instances}"
  end

  def self.total
    @instances
  end
end

entities = Array.new(9) { Entity.new }

p entities[6].who_am_i  # => "I'm 7 of 9"
p Entity.instances      # => 9
p Entity.total          # => 9

In Crytsal the equivalent code would print this instead

p entities[6].who_am_i  # => "I'm 9 of 9"  # NOTICE 9, not 7
p Entity.instances      # => 9
p Entity.total          # => 9

There are no eigenclasses in Crystal. I think thats related but I'm not sure.

@vlazar What do you consider equivalent code in Crystal?

I've got this and it has the same output:

class Entity
  class_property instances = 0

  @number : Int32

  def initialize
    self.class.instances += 1
    @number = self.class.instances
  end

  def who_am_i
   "I'm #{@number} of #{self.class.instances}"
  end

  def self.total
    @@instances
  end
end

entities = Array.new(9) { Entity.new }

p! entities[6].who_am_i  # => "I'm 7 of 9"
p! Entity.instances      # => 9
p! Entity.total          # => 9

@straight-shoota Thank you for checking this, you are right! I've messed things up apparently.

It's good Crystal has only 1 type of class variables. Less room for confusion.

Was this page helpful?
0 / 5 - 0 ratings