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 http://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 press_red_button!
  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

http://github.com/jmettraux/volute/tree/master/examples/

alternatives

states

rules

aspects

hooks and callbacks

author

John Mettraux - http://github.com/jmettraux/

feedback

license

MIT, see LICENSE.txt