Mindfuck of the day: versionable Ruby objects
# Note, some syntax inspired from that persistent data structures library for Ruby, Hamster??
# Scroll to the bottom to see it in action.
module Versioned
def self.included(base)
base.class_eval do
def self.new(*args)
new_mutable(*args).freeze
end
def self.new_mutable(*args)
allocate.tap do |instance|
instance.send :initialize, *args
instance.instance_eval do
@transformations = []
@initial_args = args
end
end
end
end
end
def mutable
self.class.new_mutable(*@initial_args).tap do |mutable|
instance_variables.each do |ivar|
mutable.instance_variable_set(ivar, instance_variable_get(ivar))
mutable.instance_variable_set(:@transformations, @transformations.dup)
end
end
end
def transform(&block)
mutable.tap do |new_instance|
new_instance.replay([block.dup])
end.freeze
end
def rollback
self.class.new_mutable(*@initial_args).tap do |instance|
instance.replay(history[0..-2])
end.freeze
end
def replay(transformations=nil)
(frozen? ? mutable : self).tap do |object|
transformations.each do |transformation|
object.history << transformation
object.instance_eval(&transformation)
end
end.freeze
end
def history
@transformations
end
def history=(transformations)
@transformations = transformations
end
def -(other)
younger, older = [history, other.history].sort { |a,b| a.length <=> b.length }
difference = (older.length - younger.length) - 1
older[difference..-1]
end
def ==(other)
history == other.history
end
end
class Person
include Versioned
def initialize
@hunger = 100
end
def eat
transform do
@hunger -= 5
end
end
def hunger
@hunger
end
def to_s
"#<Person @hunger=#{@hunger} @history=#{history.inspect}>"
end
end
# A Versionable object is immutable.
john = Person.new
puts john.frozen? # => true
# We can obtain modified instances by transforming it:
john2 = john.eat
john3 = john2.eat
john4 = john3.eat
john5 = john4.eat
latest = john5.eat
# We can go back one version:
one_step_back = latest.rollback # => this is what john5 was.
puts one_step_back == john5 # => true
# And we can calculate deltas between versions:
difference = john3 - latest
# => this is the difference between what was john3 and the latest john.
# Since deltas are just an array of individual changes, let's see what happens
# when we replay those changes on a younger version:
latest_again = john3.replay(difference)
puts latest_again == latest # => true
# Tada!!!