Lexical scoping and Ruby class variables

In this post we'll discuss some non-obvious behavior of class variables and show how it's all the fault of lexical scoping.

Ruby's class variables are confusing. Even expert Ruby users can find them hard to intuit. The most obvious example has to do with ineritance:

class Fruit
  @@kind = nil

  def self.kind
    @@kind
  end
end

class Apple < Fruit
  @@kind = "apple"
end

Apple.kind
# => "apple" 

Fruit.kind
# => "apple" 

Changing the kind variable on the child class also changes it on the parent. It's pretty messed up. But this is just how the language is supposed to work. It was a design decision made a long time ago to mimic Smalltalk.

It gets worse

There are other examples of class variable weirdness that don't seem to be architectural choices so much as implementation quirks. Today I'm going to talk a little bit about one of these that I find interesting.

We're going to compare two pieces of code now. They look like they should produce an identical result but they don't.

In our first example we set a class variable and create a method to return it. There is absolutely nothing fancy going on here. And everything works as expected.

class Foo
  @@val = 1234

  # This is shorthand for declaring a class method.
  class << self
    def val
      @@val
    end
  end
end

Foo.val
# => 1234

Perhaps you didn't know this, but class << self doesn't have to be inside the class definition. In the example below we've moved it outside. A class method is added but it can't access the class variable.

class Bar
  @@val = 1234
end

class << Bar
  def val
    @@val
  end
end

Bar.val

# warning: class variable access from toplevel
# NameError: uninitialized class variable @@val in Object

When we try to access the class variable from our function we get a warning and an exception. What's going on?

Enter lexical scope

I'm becoming more and more convinced that lexical scope is the source of 99% of the weird and confusing aspects of Ruby.

In case you're not familiar with the term, lexical scoping refers to grouping things together based on where they appear in the code, not on where they belong in an abstract object model. It's a lot easier just to show you an example:

class B
  # x and y share the same lexical scope
  x = 1
  y = 1
end

class B
  # z has a different lexical scope from x and y, even though it's in the same class. 
  z = 3
end

Class is determined Lexically

So how is lexical scope at work in our class variable example?

Well, in order to retrieve a class variable Ruby has to know which class to get it from. It uses lexical scoping to find the class.

If we look more closely at the working example we see that the code that accesses the class variable is physically with in the class definition.

class Foo  
  class << self
    def val
      # I'm lexically scoped to Foo!
      @@val
    end
  end
end

In the nonworking example, the code that accesses the class variable is not lexically scoped to the class.

class << Bar
  def val 
    # Foo is nowhere in sight. 
    @@val
  end
end

But if it's not lexically scoped to the class, what is it scoped to? The warning that Ruby prints gives us a clue: warning: class variable access from toplevel.

In the nonworking example it turns out that the class variable is lexically scoped to the top level object. This can cause some really strange behavior.

For example, if we tried to set a class variable from code that is lexically scoped to main, the class variable gets set on Object.

class Bar
end

class << Bar
  def val=(n)
    # This code is lexically scoped to the top level object. 
    # That means, that `@@val` will be set on `Object`, not `Bar`
    @@val = i
  end
end

Bar.val = 100

# Whaa?
Object.class_variables
# => [:@@foo]

More examples

There are lots of ways to reference class variables outside of the lexical scope of the class. All of them will give you trouble.

Here are a few examples for your enjoyment:

class Foo
  @@foo = :foo
end

# This won't work
Foo.class_eval { puts @@foo }

# neither will this
Foo.send :define_method, :x do 
  puts @@foo
end

# ..and you weren't thinking about using modules, were you? 
module Printable
  def foo
    puts @@foo
  end
end

class Foo
  @@foo = :foo
  include Printable
end

Foo.new.foo

And now you know why everyone says never to use class variables in Ruby. :)

What to do next:
  1. Try Honeybadger for FREE
    Honeybadger helps you find and fix errors before your users can even report them. Get set up in minutes and check monitoring off your to-do list.
    Start free trial
    Easy 5-minute setup — No credit card required
  2. Get the Honeybadger newsletter
    Each month we share news, best practices, and stories from the DevOps & monitoring community—exclusively for developers like you.
    author photo

    Starr Horne

    Starr Horne is a Rubyist and Chief JavaScripter at Honeybadger.io. When she's not neck-deep in other people's bugs, she enjoys making furniture with traditional hand-tools, reading history and brewing beer in her garage in Seattle.

    More articles by Starr Horne
    Stop wasting time manually checking logs for errors!

    Try the only application health monitoring tool that allows you to track application errors, uptime, and cron jobs in one simple platform.

    • Know when critical errors occur, and which customers are affected.
    • Respond instantly when your systems go down.
    • Improve the health of your systems over time.
    • Fix problems before your customers can report them!

    As developers ourselves, we hated wasting time tracking down errors—so we built the system we always wanted.

    Honeybadger tracks everything you need and nothing you don't, creating one simple solution to keep your application running and error free so you can do what you do best—release new code. Try it free and see for yourself.

    Start free trial
    Simple 5-minute setup — No credit card required

    Learn more

    "We've looked at a lot of error management systems. Honeybadger is head and shoulders above the rest and somehow gets better with every new release."
    — Michael Smith, Cofounder & CTO of YvesBlue

    Honeybadger is trusted by top companies like:

    “Everyone is in love with Honeybadger ... the UI is spot on.”
    Molly Struve, Sr. Site Reliability Engineer, Netflix
    Start free trial