volute
I wanted to write something about the state of multiple objects, I ended with something that feels like a subset of aspect oriented programming.
It can be used to implement toy state machines, or dumb rule systems.
include Volute
When the Volute mixin is included in a class, its attr_accessor call is modified so that the resulting attributer set method, upon setting the value of the attribute triggers a callback defined outside of the class.
require 'rubygems'
require 'volute' # gem install volute
class Light
include Volute
attr_accessor :colour
attr_accessor :changed_at
end
volute :colour do
object.changed_at = Time.now
end
l = Light.new
p l # => #<Light:0x10014c480>
l.colour = :blue
p l # => #<Light:0x10014c480 @changed_at=Fri Oct 08 20:01:52 +0900 2010, @colour=:blue>
There is a catch in this example, the volute will trigger for any class that inccludes Volute and which sees a change to its :colour attribute.
Those two classes would see their :colour hooked :
class Light
include Volute
attr_accessor :colour
attr_accessor :changed_at
end
class Flower
include Volute
attr_accessor :colour
end
To make sure that only instance of Light will be concerned, one could write :
volute Light do
volute :colour do
object.changed_at = Time.now
end
end
Inside of a volute, these are the available ‘variables’ :
-
object - the instance whose attribute has been set
-
attribute - the attribute name whose value has been set
-
previous_value - the previous value for the attribute
-
value - the new value
thus :
volute Light do
volute :colour do
puts "#{object.class}.#{attribute} : #{previous_value.inspect} --> #{value.inspect}"
end
end
l = Light.new
l.colour = :blue
l.colour = :red
would output :
Light.colour : nil --> :blue
Light.colour : :blue --> :red
filters / guards
A volute combines a list of arguments with a block of ruby code
volute do
puts 'some attribute was set'
end
volute Light do
puts 'some attribute of an instance of class Light was set'
end
volute Light, Flower do
puts 'some attribute of an instance of class Light or Flower was set'
end
volute :count do
puts 'the attribute :count of some instance got set'
end
volute :count, :number do
puts 'the attribute :count or :number of some instance got set'
end
volute Light, :count do
puts 'some attribute of an instance of class Light was set'
puts 'OR'
puts 'the attribute :count of some instance got set'
end
As soon as 1 argument matches, the Ruby block of the volute is executed. In other words, arg0 OR arg1 OR … OR argN
If you need for an AND, read on to “nesting volutes”.
Filtering on attributes who match a regular expression :
class Invoice
include Volute
attr_accessor :amount
attr_accessor :customer_name, :customer_id
end
volute /^customer_/ do
puts "attribute :customer_name or :customer_id got modified"
end
‘transition volutes’
It’s possible to filter based on the previous value and the new value (with :any as a wildcard) :
volute 0 => 100 do
puts "some attribute went from 0 to 100"
end
volute :any => 100 do
puts "some attribute was just set to 100"
end
volute 0 => :any do
puts "some attribute was at 0 and just got changed"
end
Multiple start and end values may be specified :
volute [ 'FRA', 'ZRH' ] => :any do
puts "left FRA or ZRH"
end
volute 'GVA' => [ 'SHA', 'NRT' ] do
puts "reached SHA or NRT from GVA"
end
Regular expressions are OK :
volute /^S..$/ => /^F..$/ do
puts "left S.. and reached F.."
end
volute :not
A volute may have :not has a first argument
volute :not, Invoice do
puts "some instance that is not an invoice..."
end
volute :not, :any => :delivered do
puts "a transition to something different than :delivered..."
end
volute :not, Invoice, :paid do
puts "not an Invoice and not a variation of the :paid attribute..."
end
Not Bob or Charlie, Nor Bob and neither Charlie.
nesting volutes
Whereas enumerating arguments for a single volute played like an OR, to achieve AND, one can nest volutes.
volute Invoice do
volute :paid do
puts "the :paid attribute of an Invoice just changed"
end
end
volute Grant do
volute :paid do
puts "the :paid attribute of a Grant just changed"
end
end
‘guarding’ inside of the volute block
As long as one doesn’t use a ‘return’ inside of a block, they’re just ruby code…
volute Patient do
if object.sore_throat == true && object.fever == true
puts "needs further investigation"
elsif object.fever == true
puts "only a small fever"
end
end
‘state volutes’
“I want this volute to trigger when the patient has a sore_throat and the flu”, would translate to
volute Patient do
volute :sore_throat do
if value == true && object.fever == true
puts "it triggers"
end
end
volute :fever do
if value == true && object.sore_throat == true
puts "it triggers"
end
end
end
hairy isn’t it ? There is a simpler way, it constitutes an exception to the “volute arguments join in an OR”, but it reads well (I hope) :
volute Patient do
volute :sore_throat => true, :fever => true do
puts "it triggers"
end
end
:not applies as well :
volute Patient do
volute :not, :leg_broken do
send_patient_back_home # we only treat broken legs
end
end
Our sore_throat and flu example could be rewritten with Ruby ifs as :
volute Patient do
if object.sore_throat == true && object.flu == true
puts "it triggers"
end
end
which isn’t hairy at all.
Pointing to multiple values is OK :
volute Package do
volute :delivered => true, :weight => [ '1kg', '2kg' ] do
puts "delivered a package of 1 or 2 kg"
end
end
Not mentioning an attribute implies its value doesn’t matter when matching state, :any and :not_nil could prove useful though :
volute Package do
volute :delivered => :not_nil do
puts "package entered delivery circuit"
end
end
volute Item do
volute :weight => :any, :package => true do
# dropping ":weight => :any" would make sense, but sometimes, when
# tweaking volutes, a quick editing of :any to another value is
# almost effortless
end
end
For attributes whose values are strings, regular expressions may prove useful :
volute :location => /^Fort .+/ do
puts "Somewhere in Fort ..."
end
There is an example that uses those ‘state volutes’ at github.com/jmettraux/volute/blob/master/examples/diagnosis.rb
‘over’
Each volute that matches sees its block called. In order to prevent further evaluations, the ‘over’ method can be called.
volute Package do
volute do
over if object.delivered
# prevent further volute evaluation if the package was delivered
end
volute :location do
(object.comment ||= []) << value
end
end
application of the volutes on demand
Up until now, this readme focused on the scenario where volute application is triggered by a change in the state of an attribute (in a class that includes Volute).
It is entirely OK to have classes that do not include Volute but are the object of a volute application :
class Engine
attr_accessor :state
def turn_key!
@key_turned = true
Volute.apply(self, :key_turned)
end
def
Volute.apply(self)
end
end
volute Engine do
if attribute == :key_turned
object.state = :running
else
object.state = :off
end
end
The key here is the call to
Volute.apply(object, attribute=nil, previous_value=nil, value=nil)
In fact, for classes that include Volute, this method is called for each attribute getting set.
This technique is also a key when building system where the volutes aren’t called all the time but only right before their result should matter (‘decision’ versus ‘reaction’).
volute blocks, closures
TODO
volute management
TODO
examples
github.com/jmettraux/volute/tree/master/examples/
alternatives
states
-
there is a list of ruby state machines at the end of jmettraux.wordpress.com/2009/07/03/state-machine-workflow-engine/
rules
aspects
-
github.com/search?type=Repositories&language=ruby&q=aspect&repo=&langOverride=&x=15&y=25&start_value=1 (github search)
hooks and callbacks
author
John Mettraux - github.com/jmettraux/
feedback
-
IRC freenode #ruote
license
MIT, see LICENSE.txt