This post was co-written by Jacob Evelyn.

We recently released a memoization gem, MemoWise! We’ve written about its origin story and performance. In this post, we’ll discuss some esoteric corners of Ruby we encountered while writing MemoWise.

Memoizing frozen objects with prepend

One of the features we needed to support when creating this gem was memoization of frozen, or immutable, objects. Specifically, we use the Values gem which creates immutable instances. Once an object is frozen, we can’t assign any of its instance variables:

class Example
 attr_writer :value
end
Example.new.freeze.value = true # FrozenError (can't modify frozen Example)

How is this relevant to memoization? Most memoization gems work by creating a hash to store memoized values. This hash is usually an instance variable on the object itself. (We call ours @_memo_wise.) So if the object is immutable, this instance variable can’t be assigned after the object is frozen. (It can, however, be mutated.)

This is why we prepend the MemoWise module to enable memoization. prepend is less well known than include or extend and works slightly differently than both of them.

Every Ruby class has a list of ancestors. This list contains all included or preprended modules, ordered by inheritance. We can look at Array as an example:

Array.ancestors
=> [Array, Enumerable, Object, Kernel, BasicObject]

When a method is called on an object, Ruby will look through each ancestor sequentially to see if the method is defined on any of the ancestors of that object. When we include a module, that module is inserted after the class:

module Example; end

class Array
  include Example
end

Array.ancestors
=> [Array, Example, Enumerable, Object, Kernel, BasicObject]

When we extend a module, the module’s methods are imported as methods on the class, and the module is not inserted into the ancestors chain.

class Array
  extend Example
end

Array.ancestors
=> [Array, Enumerable, Object, Kernel, BasicObject]

When we prepend a module, the module is inserted before the class prepending it in the ancestors chain. The module’s methods will take precedence over the class’ methods.

class Array
  prepend Example
end

Array.ancestors
=> [Example, Array, Enumerable, Object, Kernel, BasicObject]

This, in turn, allows us to override the initialize method, and create the memoization hash before the object is frozen. This is how MemoWise allows memoization of frozen objects.

Determining method visibility

Another important feature in MemoWise is preserving method visibility. If someone using our gem memoizes a private method, we want to guarantee that the memoized method will still be private.

There is actually not a built-in Ruby method to get a method’s visibility. However, it is possible to determine visibility by combining various built-in methods:

if private_method_defined?(method_name)
  :private
elsif protected_method_defined?(method_name)
  :protected
elsif public_method_defined?(method_name)
  :public
else
  raise NoMethodError
end

Using this :private, :protected or :public symbol, we can then dynamically set the visibility of our new method to match the original one.

Supporting objects created with allocate as well as new

In testing an early version of this gem, we encountered errors memoizing ActiveRecord classes. The errors indicated that our @_memo_wise instance variable wasn’t set, which surprised us because, as mentioned earlier, we set it in initialize.

After a lot of debugging we learned that there is a little-known way in Ruby to initialize an object without executing its initialize method! It’s called allocate, and it’s used by Rails’ ActiveRecord.

When we call new on a class, what’s happening under the hood looks something like this:

class Example
  def self.new(...)
    allocate.tap { |instance| instance.initialize(...) }
  end
end

By using allocate instead of new, Rails was bypassing our initialization of @_memo_wise. To fix this we had to overwrite allocate to also perform this initialization.

Looking Ahead

These are a few of the fun things we’ve learned about Ruby while developing MemoWise, and we plan to write about others in the future! In the meantime, please try it out or read the code on GitHub, and we’re happy to accept contributions.