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
young_andrew.age
young_andrew.frozen?
young_andrew.name.frozen?
young_andrew.age.frozen?
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
elder_andrew.age
elder_andrew.frozen?
elder_andrew.name.frozen?
elder_andrew.age.frozen?
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
young_andrew.age
young_andrew.frozen?
young_andrew.name.frozen?
young_andrew.age.frozen?
young_andrew.version
young_andrew.parent
The method update
stores reference to the #parent
and increases #version
:
elder_andrew = young_andrew.update { @age = 45 }
elder_andrew.name
elder_andrew.age
elder_andrew.version
elder_andrew.parent.equal? young_andrew
You can check the previous state of the object using method #at
:
elder_andrew.at(-2) == nil
elder_andrew.at(-1) == young_andrew
elder_andrew.at(0) == young_andrew
elder_andrew.at(1) == elder_andrew
elder_andrew.at(2) == nil
This can be used to check whether two instances has a cenancestor:
elder_andrew.at(0) == young_andrew.at(0)
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
reborn_andrew.age
reborn_andrew.version
reborn_andrew.parent
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 }
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?
name.freeze == name
Installation
Add this line to your application's 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.