Lazy Attributes in Ruby

Lazy attributes are absent from Ruby, but a bit of creativity enables them to be programmed into the language.

Definition

A lazy attribute’s value is determined the first time it’s used. Technically, attributes in Ruby start with @ and cannot be made lazy. However, blurring the line between attributes and accessors leads to three implementations of accessor-based laziness.

Naive Approach

The naive approach is often found in the wild. It uses ||= instead of = to define an instance attribute. For example, a full name can be determined based on first and last names via:

class User
  def full_name
    @full_name ||= "#{first_name} #{last_name}"
  end
end

If more than one statement is needed to determine the value they can be wrapped in a block.

This approach is succinct, but suffers from a serious, even fatal, flaw: nil and false will result in duplicate computations. attribute ||= value is equivalent to attribute = attribute || value, so if attribute is falsey value will always be evaluated.

Fortunately, this drawback can be eliminated.

Robust Approach

The naive approach conflated two things: checking for attribute’s existence and checking its boolean value. The ||= trick works because referencing an undefined attribute returns nil. A robust implementation of lazy attributes requires an explicit existence check.

Ruby offers defined? to do exactly that. For example, full_name from the previous section can be implemented as:

def full_name
  return @full_name if defined?(@full_name)

  @full_name = "#{first_name} #{last_name}"
end

Correctness was gained at the expense of brevity. Luckily, there is a way to regain that brevity.

Meta-programming

Ruby’s flexibility enables enhancing the language with a new construct that implements the robust approach. It’s actually much simpler than it sounds.

We’ll add a method named lazy to Class, so that it can be used in class definitions. It will take the attribute name as an argument and a block to determine its value. Let’s have a look at the implementation before discussing it:

def lazy name, &definition
  variable_name = :"#@{name}"

  define_method(name) do
    if instance_variable_defined? variable_name
      instance_variable_get variable_name
    else
      instance_variable_set variable_name, instance_eval(&definition)
    end
  end
end

There are a few things worth commenting.

First, variable_name is set prior to method definition to avoid recomputing it on every method call.

Second, instance_variable_defined? is used instead of defined?. The reason is defined? is a special construct; defined?(variable_name) would always return true because there is a variable named variable_name.

Third, definition has to be evaluated within the instance via instance_eval. It cannot be called via definition.call, as its body wouldn’t have access to instance attributes. For example, @first_name and @last_name from the previous example would be inaccessible.

The big question is: where should lazy be put? In the olden days of monkey-patching it’d be defined on Class directly or inside a module to extend specific classes with (which isn’t that bad). However, Ruby offers refinements for exactly that kind of patches. The LazyAttribute refinement could look like this:

module LazyAttribute
  # When Lazy is "activated" via using, patch the following class:
  refine Class do
    # Define the following method inside that class:
    def lazy name, &definition
      # ...
    end
  end
end

After the refinement is defined it’s enough to call using LazyAttribute inside a class definition to gain access to lazy. Meta-programming doesn’t have to be scary! The following snippet compares the before and after versions of a lazy accessor. It’s clear lazy resulted in increased legibility.

# Without LazyAttribute
class User
  def full_name
    return @full_name if defined?(@full_name)
    @full_name ||= "#{first_name} #{last_name}"
  end
end

# With LazyAttribute
class User
  extend LazyAttribute

  lazy(:full_name) { "#{first_name} #{last_name}" }
end

Closing Thoughts

Ruby is elegant and extensible, which enables developers to enhance it with new facilities. It’s easy to abuse that power, but it does not mean it shouldn’t be used at all, especially that refinements are available.

I’m working on a gem that will enhance Ruby with that and other features borrowed from other programming languages. Expect to see more articles and updates in the coming months.

Enjoyed the article? Follow me on Twitter!

I regularly post about Ruby, Ruby on Rails, PostgreSQL, and Hotwire.

Leave your email to receive updates about articles.