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 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
orrake 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.