Defining Immutable Record Types in Ruby with Hamsterdam

Hamsterdam is a Ruby gem that lets you define and use your own immutable record types in Ruby, based on Hamster’s immutable Hamster::Hash. Think Struct with a Hash-friendly constructor… but you can’t change any of the fields within the object once it’s created.

I’ve been enjoying Hamster on some recent Ruby and JRuby projects, but in addition to values that don’t change, I also prefer to use record types over unconstrained types like OpenStruct or Hash (or even Hamster::Hash). Hamsterdam was created to combine the benefits of declarative record types with Hamster’s efficient immutable data structures.

Installing Hamsterdam

$ gem install hamsterdam

(Hamsterdam depends on the Hamster gem, which should automatically install as a dependency.)

Hamsterdam Features

Declaring new record types is easy. Just provide a list of field names to Hamsterdam::Struct.define:

  Planet = Hamsterdam::Struct.define(:name, :orbit, :description)

Instantiating new records is just as easy: provide either a Ruby Hash or a Hamster::Hash to the constructor:

  earth = Planet.new(name: "Earth", orbit: 3, description: "Harmless.")

Since Hamsterdam’s Struct instances are immutable (they are meant to be treated as values), you cannot alter the field values of an existing object. A record’s field setters will return a new (efficiently-created) value with the field updated accordingly:

  earth1 = earth.set_description("Mostly harmless.")

Note that the earth value is itself unchanged, and only the newly created earth1 value bears the new description of “Mostly harmless.”

Because setters return values, it’s easy to chain a series of updates:

  mars = Planet.new.
    set_name("Mars").
    set_description("Dusty and red").
    set_orbit(4)

Hamsterdam records are comparable via equal?, eql? and == (delegated to Hamster::Hash implementations for each), and the #hash method is properly implemented such that values may be used as members in sets and keys in hashtables.

Benefits of Hamsterdam

  • Type Documentation – Record types are declared in an easy-to-read fashion. Others will know immediately what goes into these records and can easily modify their structure.
  • Runtime Type Checking (sorta) – You can’t construct, set values on, or get data from a Hamsterdam::Struct using incorrect field names. This means your unit and system tests will explode the moment you make a typo, or if you change or remove field names without updating all parts of the code. Hash, OpenStructs, and stubbed-out mocks won’t help you here.
  • Easier to Use == More Frequent Use – Declaring and using these records is so simple, I’m doing it more often to declare complex method signatures (as opposed to just Hashes simulating named args). I definitely prefer using them in unit and system tests as opposed to stubs or mocks for value-type objects.
  • Immutable Structures Force you to Distinguish between Values and Identities – Conceptually, values don’t change; only things that have values change… from one value to the next. I’m finding that the more I plan ahead and understand this distinction, the easier it is to think about what’s happening in my programs.

Caveat

Hamsterdam::Structs are not automatic drop-in replacements for Ruby’s Struct, Hash, or any other object whose inner state can be changed after creation.

The reason is that the changes to your code are more than syntactic. It’s not simply a different method call to change a field value. It’s a matter of who owns the data, who refers to it, and how and when that data can be changed. Any code you’ve written that expects a getter to return potentially changing values over time simply cannot work. The more you adopt the use of immutable objects in your OO code, the more it will affect the overall design of that code.

Give It a Try

If you’re interested in using more immutable structures in your code, check out Hamster. And if you like the idea of immutable record types, give Hamsterdam a try. Let me know how it works for you, or if you’ve got any questions, gripes, or issues.