Immutability

Gem Version Build Status Dependency Status Code Climate Coverage Inline docs

Makes instances immutable (deeply frozen) and versioned.

Preamble

The project is a clone of the aversion gem by Josep M. Bach with some implementation differencies:

  • it uses ice_nine gem to freeze instances deeply.
  • instead of storing procedures that changed the instance, it stores reference to the previous state and the number of current version.

This approach to object's identity as a sequence of immutable snapshots is heavily inspired by 2009 year's brilliant talk "Are We There Yet?" by Rich Hickey.

Synopsis

Immutable objects without memory:

Include the Immutability module to make the object immutable (deeply frozen).

require "immutability"

class User
  include Immutability

  attr_reader :name, :age

  def initialize(name, age)
    @name = name
    @age  = 44
  end
end

young_andrew = User.new "Andrew", 44
young_andrew.name         #  => "Andrew"
young_andrew.age          #  => 44

# The instance is frozen deeply:
young_andrew.frozen?      #  => true
young_andrew.name.frozen? #  => true
young_andrew.age.frozen?  #  => true

Use update with a block to create a new instance with updated values (other instance values remains the same):

elder_andrew = young_andrew.update { @age = 45 }
elder_andrew.name         #  => "Andrew"
elder_andrew.age          #  => 45

# The instance is frozen deeply:
elder_andrew.frozen?      #  => true
elder_andrew.name.frozen? #  => true
elder_andrew.age.frozen?  #  => true

Immutable objects with memory

Include Immutability.with_memory module to add version and parent:

require "immutability"

class User
  include Immutability.with_memory

  attr_reader :name, :age

  def initialize(name, age)
    @name = name
    @age  = 44
  end
end

young_andrew = User.new "Andrew", 44
young_andrew.name         #  => "Andrew"
young_andrew.age          #  => 44

# The instance is frozen as well:
young_andrew.frozen?      #  => true
young_andrew.name.frozen? #  => true
young_andrew.age.frozen?  #  => true

# Now it is versioned:
young_andrew.version      # => 0
young_andrew.parent       # => nil

The method update stores reference to the #parent and increases #version:

elder_andrew = young_andrew.update { @age = 45 }
elder_andrew.name    #  => "Andrew"
elder_andrew.age     #  => 45

# Version is updated:
elder_andrew.version # => 1
elder_andrew.parent.equal? young_andrew # => true

You can check the previous state of the object using method #at:

# relative from the current version
elder_andrew.at(-2) == nil          # => true
elder_andrew.at(-1) == young_andrew # => true

# at some version in the past
elder_andrew.at(0)  == young_andrew # => true
elder_andrew.at(1)  == elder_andrew # => true
elder_andrew.at(2)  == nil          # => true

This can be used to check whether two instances has a cenancestor:

elder_andrew.at(0) == young_andrew.at(0) # => true

Notice, than no instances in the sequence can be garbage collected (they still refer to each other).

Use #forget_history methods to reset version and free old instances for GC:

reborn_andrew = elder_andrew.forget_history
reborn_andrew.name # => "Andrew"
reborn_andrew.age  # => 45

# History is forgotten
reborn_andrew.version # => 0
reborn_andrew.parent  # => nil

RSpec

be_immutable

Include immutability/rspec and use be_immutable RSpec matcher to check, whether an instance is deeply immutable (with all its variables):

include "immutability/rspec"

describe User, ".new" do
  subject { User.new "Andrew", 44 }

  it { is_expected.to be_immutable }
end

The matcher will pass if both the object and all its variables are immutable at any level of nesting.

frozen_double

Initializers of immutable objects freeze their variables deeply. When you send doubles as arguments for this initializer, RSpec will warn you about trying to freeze that double. To avoid the problem, use frozen_double instead of double:

include "immutability/rspec"

describe User, "#name" do
  subject { User.new(name, 44).name }

  let(:name) { frozen_double :name, to_s: "Andrew" }

  it { is_expected.to eql "Andrew" }
end

The method returns an rspec double with two methods added:

name = frozen_double :name, to_s: "Andrew"
name.frozen?        # => true
name.freeze == name # => true

Installation

Add this line to your application's Gemfile:

# Gemfile
gem "immutability"

Then execute:

bundle

Or add it manually:

gem install immutability

Compatibility

Tested under rubies compatible to MRI 1.9+.

Uses RSpec 3.0+ for testing and hexx-suit for dev/test tools collection.

Contributing

  • Read the STYLEGUIDE
  • Fork the project
  • Create your feature branch (git checkout -b my-new-feature)
  • Add tests for it
  • Run rake mutant or rake exhort to ensure 100% mutant-proof coverage
  • Commit your changes (git commit -am '[UPDATE] Add some feature')
  • Push to the branch (git push origin my-new-feature)
  • Create a new Pull Request

License

See the MIT LICENSE.