Immutability
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 mutantorrake exhortto 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.