Expressing the problem

Mike Burns

Object-oriented languages are good at adding new data; functional languages are good at adding new behavior. Can we find a happy medium?

The answer: Sorta.

First I’ll explain what I’m talking about, with examples. At the bottom of this article I’ll talk about the lambda calculus. Oh and before that I’ll talk about something relevant to normal Ruby programming, in case that’s something you’re still into.

The problem

But first let’s start even simpler: we are going to represent addition in Ruby.

class Literal
  def initialize(n)
    @n = n
  end

  def evaluate
    @n
  end
end

class Addition
  def initialize(a, b)
    @a = a
    @b = b
  end

  def evaluate
    @a.evaluate + @b.evaluate
  end
end

No magic going on here. We can evaluate an expression:

ruby-1.9.2-p290> Addition.new(Literal.new(2), Addition.new(Literal.new(3), Literal.new(5))).evaluate
=> 10

So the claim is that it’s easy to add new data. What that means is that we can add a new class and use it quickly:

class Boolean
  def initialize(bool)
    @bool = bool
  end

  def evaluate
    @bool
  end
end

class IfThenElse
  def initialize(b, t, f)
    @b = b
    @t = t
    @f = f
  end

  def evaluate
    if @b.evaluate
      @t.evaluate
    else
      @f.evaluate
    end
  end
end

Above we have added Booleans and conditionals to our language. We haven’t added the concept of “less than” or “equal to zero” or anything like that, so for now we can only hard-code truth and lies.

ruby-1.9.2-p290> IfThenElse.new(
  Boolean.new(true),
  Addition.new(Literal.new(5),
               Addition.new(Literal.new(3), Literal.new(2))),
  Literal.new(0)).evaluate
=> 10

Further, the claim is that it’s hard to add new behavior. As an example of what that means, consider adding to_s for our little arithmetic language. Here are some ways to do that:

  • Open each class and add a to_s method. This can be done either by modifying the file directly (very possible if it’s in our codebase), or by using Ruby’s open classes. Ruby’s open classes are brittle when dealing with private data, so we should consider avoiding that.
  • Define subclasses, either via composition or inheritance, that add the desired debugging behavior. This would mean re-writing every use of IfThenElse.new with DebuggingIfThenElse.new, as needed.

Both of those options are OK, and will absolutely work perfectly in some situations. Let’s explore another option: abstract factories.

A solution

The abstract factory pattern is described like this:

Provide an interface for creating families of related or dependent objects without specifying their concrete classes.

We can define (and name!) our system as an abstract factory:

class AdditionAndNumbers
  def literal(n)
    Literal.new(n)
  end

  def addition(a, b)
    Addition.new(a, b)
  end
end

And we can make use of this as before:

ruby-1.9.2-p290> system = AdditionAndNumbers.new
ruby-1.9.2-p290> system.addition(system.literal(1), system.literal(3)).evaluate
=> 10

We can add new data types to our system:

class BooleansWithAdditionAndNumbers < AdditionAndNumbers
  def boolean(b)
    Boolean.new(b)
  end

  def if_then_else(b, t, f)
    IfThenElse.new(b, t, f)
  end
end

And make use of those, too:

ruby-1.9.2-p290> system = BooleansWithAdditionAndNumbers.new
ruby-1.9.2-p290> system.if_then_else(
  system.boolean(true),
  system.addition(
    system.literal(5),
    system.addition(
      system.literal(3),
      system.literal(2))),
  system.literal(0)).evaluate
=> 10

And we can add our desired stringification. Here’s where the “sorta” in my introduction shines:

class ShowLiteral
  def initialize(n)
    @n = n
  end

  def to_s
    @n.to_s
  end
end

class ShowAddition
  def initialize(a, b)
    @a = a
    @b = b
  end

  def to_s
    "#{@a.to_s} + #{@b.to_s}"
  end
end

class ShowAdditionAndNumbers
  def literal(n)
    ShowLiteral.new(n)
  end

  def addition(a, b)
    ShowAddition.new(a, b)
  end
end

Here I’ve defined a new abstract factory that produces objects that respond to to_s usefully. The code that uses this system does not change, as it still calls literal and addition as before, except now it calls to_s instead of evaluate.

ruby-1.9.2-p290> system = ShowAdditionAndNumbers.new
ruby-1.9.2-p290> system.addition(system.literal(1), system.literal(3)).to_s
=> "1 + 3"

In fact, it doesn’t even need to be that way: the to_s method could instead have been called evaluate. However, this produces a String instead of a Fixnum, so that’s weird.

The complaint

That’s a lot of small classes!

Boo hoo. Don’t use this pattern unless you need it.

Another example

Factory Bot has a bunch of strategies for factorying-up some data. Here are some massive simplifications, for example:

class Build
  def initialize(class_name, attributes)
    @class_name = class_name
    @attributes = attributes
  end

  def run
    @class_name.to_s.camelize.constantize.send(:new, @attributes)
  end
end

class Create
  def initialize(class_name, attributes)
    @class_name = class_name
    @attributes = attributes
  end

  def run
    @class_name.to_s.camelize.constantize.send(:create, @attributes)
  end
end

New strategies are easy to add, but new behavior is not. Again I’ll use the example of inspecting into a String, but this time I’ll make use of the existing run method instead of a new to_s method. Again, just an example.

We start by making an abstract factory:

class Strategy
  def build(class_name, attributes)
    Build.new(class_name, attributes).run
  end

  def create(class_name, attributes)
    Create.new(class_name, attributes).run
  end
end

Here we’re going to go on a tangent to add an extension point to Factory Bot. This is because, given the above change, Factory Bot will have some code like:

class FactoryBot
  def build(class_name, attributes = {})
    strategy.build(class_name, attributes)
  end

  def create(class_name, attributes = {})
    strategy.create(class_name, attributes)
  end

  def plugin(strategy_name, class_name, attributes = {})
    strategy.send(strategy_name, class_name, attributes)
  end

  private

  def strategy
    Strategy.new
  end
end

That private method is brutal. So it instead needs to be:

class FactoryBot
  def self.strategy=(s)
    @@strategy = s
  end

  # ...

  private

  def strategy
    if defined?(@@strategy)
      @@strategy
    else
      Strategy.new
    end
  end
end

Whew. Tangent over.

This allows us to define StubbingStrategy:

require 'mocha'

class Stub
  def initialize(class_name, attributes)
    @class_name = class_name
    @attributes = attributes
  end

  def run
    class_object.new.tap do |s|
      @attributes.each do |method, result|
        s.stubs(method).returns(result)
      end
    end
  end

  private

  def class_object
    @class_name.to_s.camelize.constantize
  end
end

class StubbingStrategy < Strategy
  def stub(class_name, attributes)
    Stub.new(class_name, attributes).run
  end
end

And then use it:

FactoryBot.strategy = StubbingStrategy.new
user_stub = FactoryBot.new.plugin(:stub, :user)

Add behavior

We’re back to where we started; great. But, now we can add behavior on a whim:

class DebuggingBuild
  def initialize(class_name, attributes)
    @class_name = class_name
    @attributes = attributes
  end

  def run
    "#{@class_name.to_s.camelize}.new(#{@attributes.inspect})"
  end
end

class DebuggingCreate
  def initialize(class_name, attributes)
    @class_name = class_name
    @attributes = attributes
  end

  def run
    "#{@class_name.to_s.camelize}.create(#{@attributes.inspect})"
  end
end

class DebuggingStrategy
  def build(class_name, attributes)
    DebuggingBuild.new(class_name, attributes).run
  end

  def create(class_name, attributes)
    DebuggingCreate.new(class_name, attributes).run
  end
end

This is a strategy that causes the normal build and create strategies to produce strings instead of the normal data. We don’t need to change anything else: just set this as the strategy and use it:

ruby-1.9.2-p290> FactoryBot.strategy = DebuggingStrategy.new
ruby-1.9.2-p290> FactoryBot.new.create(:user)
=> "User.create({})"

Read more

The gory details, including the arithmetic system example, come from this year’s ECOOP in a paper titled “Extensibility for the Masses: Practical Extensibility with Object Algebras” (PDF) by Bruno C.d.S. Oliveira and William R. Cook.

I recommend reading the paper for more cool examples, such as concurrent computation of the arithmetic system and a DSL for batch processing. They also relate Church encodings to the visitor pattern, and abstract factories to F-algebras. Throughout the paper they talk about static type analysis, too.


Disclaimer:

Looking for FactoryGirl? The library was renamed in 2017. Project name history can be found here.