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 variables deeply. When you use doubles as initializers' arguments, RSpec will complain. To avoid this problem, use frozen_double instead of double:

include "immutability/rspec"

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

  # the name can be used by the initializer safely
  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.