Saturday, 12 September 2020

Beware the class level instance var

  • ruby
  • travels-in-ruby

I've worked with a lot of Ruby codebases in my career, some in better shape than others. One bug bear of mine is that many would-be Ruby developers, or those coming from other languages seem to use a class method (e.g self.method) in place of a good ole instance method. In less than desirable codebases you'll often find:

class MyService
  def self.method(args)
    # Do some stuff

Soon the functionality of the class above will need to be extended, and an inexperienced developer will throw in some state without realising the ramifications of doing so in a self method:

class MyService
  def self.method(args)
    # Do some stuff
    @an_instance_variable = result_of_above
  def self.another_method
    puts @an_instance_variable

When I saw the usage of instance variables like this in class methods, I baulked. But it turns out it does work:

irb(main):055:0> class MyService
irb(main):056:1>   def self.method(args)
irb(main):057:2>     # Do some stuff
irb(main):059:2>     @an_instance_variable = 1
irb(main):060:2>   end
irb(main):062:1>   def self.another_method
irb(main):063:2>     puts @an_instance_variable
irb(main):064:2>   end
irb(main):065:1> end
=> :another_method
irb(main):066:0> MyService.method({})
=> 1
irb(main):067:0> MyService.another_method
=> nil

So a Ruby class can have class level instance variables 🤯. This was news to me. To take things further, you can assign a class level instance variable outside of a method and the value will still be persisted in class methods:

class MyService
  @an_instance_variable = 2

  def self.another_method
    puts @an_instance_variable

Here is the irb to prove it:

irb(main):085:0> MyService.another_method

To strip things back even further:

irb(main):086:0> class MyService
irb(main):087:1>   @an_instance_variable = 2
irb(main):088:1> end
=> 2
irb(main):089:0> MyService.instance_variables
=> [:@an_instance_variable]

So, how can this be the case? Well:

Because each class is an object, it can have instance variables just like any other Ruby object.

I'm not sure why you would want to introduce such a niche feature of Ruby into your codebase, but knock yourself out (if you are OK with unintended consequences of such).

Addendum: the Eigenclass

I have often seen class methods using the self.method syntax and then the weird looking class << self syntax used interchangeably. The latter syntax is something called an Eigenclass, or more simply a Singleton class:

class MyService
  # Approach A
  def self.method
  # Approach B
  class << self
    def method

The two approaches above are not equivalent!

irb(main):102:0> class Hi
irb(main):103:1>   self #=> Hi
irb(main):105:1>   class << self
irb(main):106:2>     self #=> #<Class:Hi>
irb(main):107:2>     self == Hi.singleton_class #=> true
irb(main):108:2>   end
irb(main):109:1> end
=> true
irb(main):111:0> Hi.itself
=> Hi
irb(main):112:0> Hi.singleton_class
=> #<Class:Hi>

self in Ruby usually refers to the instance of a class when you call it in that context. However self in the example above can also refer to the Hi class itself, as after all, all Ruby classes are still objects.

What the class << self syntax is saying is to create an anonymous class based off of self which in the lexical scope is the class Hi (and given that class << self is a special syntax that signifies a Singleton, then assign the new anonymous class to Hi.singleton_class). The outputs above prove that to be the case.