2

Working on a Ruby program I was looking to move some state data from instance variables to class variables, it dawned on me that while instance variables are auto-vivified (if you try to read them "without initializing" them, they are automatically initialized to nil), class variables are not - and this looks very inconsistent to me (compared to most Ruby syntax which is very consistent).

Sample program:

class Test
  def id
    @id.to_i
  end
  def id=(i)
    @id = i
  end
  def nextid
    self.id = id + 1
  end
end

t = Test.new
puts t.nextid #=> 1
puts t.nextid #=> 2

In which case, when calling Test::id, if @id was not initialized, Ruby will auto-vivify it to nil (after which I to_i it to get 0).

Now I decide that I want the running ID to be shared across Test instance, so I rewrite it like this:

class Test
  def id
    @@id.to_i
  end
  def id=(i)
    @@id = i
  end
  def nextid
    self.id = id + 1
  end
end

t = Test.new
puts t.nextid
puts t.nextid

Should work the same, I thought, but no:

NameError: uninitialized class variable @@id in Test

But this workaround works (!?) :

class Test
  def id
    (@@id ||= 0).to_i
  end
  def id=(i)
    @@id = i
  end
  def nextid
    self.id = id + 1
  end
end

t = Test.new
puts t.nextid #=> 1
puts t.nextid #=> 2

(granted, after doing lazy init to 0 I can drop the to_i, but I left it for consistency).

It looks like Ruby understands "lazy initialization" and treats it as the magic needed to not throw NameError - even though ||= is supposedly just syntactic sugar to x = x || val (which BTW doesn't work for initing class variables, thanks for asking).

How come?

Guss
  • 30,470
  • 17
  • 104
  • 128
  • 3
    `x ||= val` is kinda equivalent to `x || x = val`, not `x = x || val`. Also, in your code, why would you want an instance method to set a class variable? – Eric Duminil May 08 '17 at 11:58
  • Which ruby version are you tested ? I have tried the second case in ruby-2.2.2, I got `NoMethodError: undefined method '+' for nil:NilClass` instead of `NameError: uninitialized class variable @@id` – Fangxing May 08 '17 at 12:07
  • @fangxing: I don't think it depends on Ruby version. I'd say it's a typo in the question. You can use `self.id = id + 1` to get the mentioned error. – Eric Duminil May 08 '17 at 12:09
  • `||=` works just fine for initializing class variables. What's your problem, exactly? – Eric Duminil May 08 '17 at 12:15
  • @EricDuminil - re `||=` is that for instance variables I don't need it. Why do class variable require it and instance variables do not? also, `x || x = val` also wont work if `x` is `@@x`. – Guss May 08 '17 at 12:22
  • " it dawned on me that while instance variables are auto-vivified (...), instance variables are not" What is meant here? – steenslag May 08 '17 at 16:17
  • @steenslag: "class variables", very probably. – Eric Duminil May 08 '17 at 19:37
  • @steenslag, sorry - fixed mis-typing – Guss May 09 '17 at 07:58
  • @EricDuminil - your point about the behavior of `||=` is very good and this article explains why it works in my last example and why I shouldn't be surprised: http://www.rubyinside.com/what-rubys-double-pipe-or-equals-really-does-5488.html – Guss May 09 '17 at 09:09
  • I see it here just fine... – Guss May 09 '17 at 09:17
  • I can't edit comments - your comments are not part of the post. your points about `||=` are in the comments and are likely just collapsed for you by default. – Guss May 09 '17 at 09:25

1 Answers1

3

Class variables initialization

Here's a possible explanation why @a is nil but @@a is a NameError.

But if you want to use class variables, you should initialize them inside the class, not inside instance methods :

class Test
  @@id = 0

  def id
    @@id
  end

  def id=(i)
    @@id = i
  end

  def nextid
    self.id = id + 1
  end
end

t = Test.new
puts t.nextid
puts t.nextid

Please note that it doesn't make much sense to have an instance setter method for a class variable.

Class instance variables

To avoid mixing instance methods and class variables, you could define everything at the class level with a "class instance variable". It's an instance variable defined at the class level:

class Test
  @id = 0
  class << self
    def id
      @id
    end

    def id=(i)
      @id = i
    end

    def nextid
      self.id = id + 1
    end
  end
end

puts Test.id
# 0
puts Test.nextid
# 1
puts Test.nextid
# 2

It means you could just use attr_accesor:

class Test
  @id = 0
  class << self
    attr_accessor :id
    def nextid
      self.id = id + 1
    end
  end
end
Community
  • 1
  • 1
Eric Duminil
  • 52,989
  • 9
  • 71
  • 124
  • Ah, my apologies. I didn't actually read past that line, got insta-triggered. Shame on me! – Sergio Tulentsev May 08 '17 at 12:19
  • "Also, you claim that ||= doesn't work for class variables but it's apparently wrong" - I specifically did not claim that, please read the OP. The question was *why* it works for class variables while "read before write" doesn't. – Guss May 09 '17 at 08:00
  • Thanks, but as I seem to have failed to point out, I'm not interested in a solution (as I've already presented one, albeit its an ugly solution) but more in the question *why* things are as they are. The "possible explanation" you pointed out, basically boils down to "it makes sense in the following edge cases", which is hardly a design reason. – Guss May 09 '17 at 09:02
  • @Guss: I totally agree it's not a completely satisfying answer. It's more like a log of my investigation. I don't even know if there's a definitive answer why `@@z` is undefined. Anyway, I dare say the last piece of code is a clean refactoring of your code, without any class variable. class variables are a thing of the past anyway, and shouldn't be used in recent projects. – Eric Duminil May 09 '17 at 09:04
  • Your note about "class variables are deprecated" is interesting - care to share a reference to a relevant discussion by the Ruby community? – Guss May 09 '17 at 09:10
  • @Guss: https://github.com/bbatsov/ruby-style-guide#no-class-vars and https://makandracards.com/makandra/14229-the-many-gotchas-of-ruby-class-variables for example. – Eric Duminil May 09 '17 at 09:18
  • I've seen such discussions in the past. It boils down to "class variables behave like Java or C++ static variables and are thus not what you expect and dangerous". I find this approach baffling as Java and C++ developers like this feature and use it successfully, and I believe so do (at least some) Ruby developers. – Guss May 09 '17 at 09:27