Ruby Singleton Objects

avdi

Avdi Grimm

Posted on November 8, 2020

Ruby Singleton Objects

Here’s another freebie from the deep RubyTapas stacks. This one is about a truth of object modeling that we don’t often talk about: not every object needs to have state. If an object has no state, there’s no need to have more than one of it. And for stateless objects, having a class just to generate a single instance may be superfluous!

Director’s commentary: In retrospect I’d tighten up the pacing on this one. And I hate that the ending monologue on the Singleton pattern has no illustration on the screen. Also, I really hope I don’t sound that bored in more recent videos.

While I was at Ruby DCamp a few weeks ago Sandi Metz asked me to write a version of Conway’s Game of Life in a semi-functional, stateless style. I decided to represent the concept of a “live cell” and a “dead cell” as two different kinds of object. The game grid would then be represented by a simple array of arrays, populated by live cells and dead cells.

I was all set to write a class for each…

class LiveCell
  # ...
end

class DeadCell
  # ...
end

Enter fullscreen mode Exit fullscreen mode

…when it occurred to me: since this is a stateless implementation, there was no real need to have individual LiveCell objects for every live cell on the board. And the same goes for dead cells. Without any state, every instance would be exactly the same. And since the objects didn’t have any need for initialization, why even bother with classes?

Instead, I created the objects as singleton instances of the Object class. I assigned the instances to constants, so they would be available anywhere in the program. Then I used Ruby’s singleton class syntax to add methods to each one. Each object needed two methods: a #to_s method to represent the cell on an ASCII grid, and a method to determine what the next generation of its grid square would contain: either a live cell or a dead cell.

LIVE_CELL = Object.new
class << LIVE_CELL
  def to_s() 'o' end

  def next_generation(x, y, board)
    case board.neighbors(x,y).count(LIVE_CELL)
    when 2..3 then self
    else DEAD_CELL
    end
  end
end

DEAD_CELL = Object.new
class << DEAD_CELL
  def to_s() '.' end

  def next_generation(x, y, board)
    case board.neighbors(x,y).count(LIVE_CELL)
    when 3 then LIVE_CELL
    else self
    end
  end
end

Enter fullscreen mode Exit fullscreen mode

Then I could just populate my grid with the same LIVE_CELL and DEAD_CELL objects, over and over again.

[
 [DEAD_CELL, LIVE_CELL, LIVE_CELL, DEAD_CELL],
 # ...
]

Enter fullscreen mode Exit fullscreen mode

This worked quite well, but I didn’t like the fact that the singleton objects had to be created in two steps. So I decided to see if I could create the object, assign it to a constant, and define methods on it, all in a single statement.

To accomplish this feat, I took advantage of the fact that Ruby allows variables and constants to be assigned inside parenthesized sub-expressions of a statement.

class << (LIVE_CELL = Object.new)
  def to_s() 'o' end

  def next_generation(x, y, board)
    case board.neighbors(x,y).count(LIVE_CELL)
    when 2..3 then self
    else DEAD_CELL
    end
  end
end

class << (DEAD_CELL = Object.new)
  def to_s() '.' end

  def next_generation(x, y, board)
    case board.neighbors(x,y).count(LIVE_CELL)
    when 3 then LIVE_CELL
    else self
    end
  end
end

Enter fullscreen mode Exit fullscreen mode

The resulting syntax was a bit obscure, but it accomplished my purpose succinctly.

A class plays two roles in an OO program:

  1. It provides a container for behavior that’s shared by many objects.
  2. It acts as an object factory, manufacturing new instances and ensuring they are initialized correctly.

When we have an object which does not need to share behavior with any others objects, and which requires no initialization, that renders both roles of a class superfluous. In cases like this, it can make more sense to just use a one-off singleton object.

Another way to accomplish this is to use a module as our singleton object, and only define class-level methods on the module. Let’s update the example to use this approach instead.

module LiveCell
  def self.to_s() 'o' end

  def self.next_generation(x, y, board)
    case board.neighbors(x,y).count(LiveCell)
    when 2..3 then self
    else DeadCell
    end
  end
end

module DeadCell
  def self.to_s() '.' end

  def self.next_generation(x, y, board)
    case board.neighbors(x,y).count(LiveCell)
    when 3 then LiveCell
    else self
    end
  end
end

[
 [DeadCell, LiveCell, LiveCell, DeadCell],
 # ...
]

Enter fullscreen mode Exit fullscreen mode

This is a perfectly legitimate way to implement singleton objects. Personally, I feel that using a singleton instance of Object is a little more intention-revealing, since modules are usually used as namespaces and as a means of sharing behavior. But I don’t feel that strongly about it. The main thing I hope to convey in this episode is simply that sometimes a singleton object is all we need, and when that’s all we need, that’s all we should use.

I should make one last point here: I’m talking about singleton objects, not the Singleton Pattern. The Singleton Pattern describes a way to control the construction of a particular class such that only one instance is ever constructed. Singletons following this pattern have often been used to manage a great deal of centralized state, a situation that can lead to a number of problems, including thread contention and proliferation of dependencies. The singleton objects we’ve defined here are stateless and more akin to the immutable singletons that Ruby provides like true, false, and nil. As such they are somewhat less prone to abuse.

Okay, enough for today. Happy hacking!

💖 💪 🙅 🚩
avdi
Avdi Grimm

Posted on November 8, 2020

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related

Ruby Singleton Objects
rubytapasfreebies Ruby Singleton Objects

November 8, 2020