Skip to content

Instantly share code, notes, and snippets.

@ericgj
Created October 17, 2011 05:59
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save ericgj/3c72bd3467fceccbb930 to your computer and use it in GitHub Desktop.
Save ericgj/3c72bd3467fceccbb930 to your computer and use it in GitHub Desktop.
Methods taking multiple blocks

In my opinion, one of many beautiful constraints of Ruby is the ability to send only one lambda function into a method. Why beautiful? Because it stops us from suffering the madness of interfaces where you have to remember not only where exactly they are used in the method, whether or not the return values are significant, whether or not they change some bit of state, etc., but also now the order of the parameters.

One lambda is hard enough! This isn't javascript, we do have a class system to help us out...!

There are any number of ways to do this, the following is one idea that came to mind. It's got more magic that I usually like, but I split the implementation into a "Basic" module you can use if you just want the observable-ish behavior, and not the sugar (AKA method name pollution).

module StateMachine
FailedTransition = Class.new(StandardError)
IllegalTransition = Class.new(StandardError)
def self.included(klass)
klass.send(:include, Basic)
klass.extend(ClassMethods)
end
module ClassMethods
# pre-define callbacks to trip illegal transition errors
# (transitions other than defined in def_state)
def new(*args, &blk)
obj = allocate
watched_attributes.each do |attr|
valid = valid_transitions(attr).inject({}) { |memo, pair|
(memo[pair[1]] ||= []) << pair[0]
memo
}
valid.each do |(to, froms)|
obj.on(attr,to) do
if froms.include?(obj.send(attr))
true
else
raise StateMachine::IllegalTransition, "from #{obj.send(attr)} to #{to}"
end
end
end
end
obj.send(:initialize, *args, &blk)
obj
end
def valid_transitions(attr)
_transitions[attr]
end
def def_state(attr, transitions)
case transitions
when Symbol
def_state(attr, {nil => transitions})
when Hash
transitions.each do |(from, tos)|
(Enumerable === tos ? tos : [tos]).each do |to|
_add_transition attr, from, to
_add_callback_shortcuts to
end
end
else
raise ArgumentError, "expected hash or symbol"
end
end
private
def _transitions; @_transitions ||= Hash.new {|h,k| h[k] = []}; end
def _add_transition(attr, from, to)
_transitions[attr] << [from, to]
end
def _add_callback_shortcuts(to)
unless method_defined?("#{to}!")
define_method "#{to}!" do |attr|
self.send("#{attr}=", to)
end
end
unless method_defined?("#{to}?")
define_method "#{to}?" do |attr|
self.send(attr) == to
end
end
unless method_defined?("on_#{to}")
define_method "on_#{to}" do |attr, &blk|
on(attr, to, &blk)
end
end
end
end
# include StateMachine::Basic if you don't want the metaprogramming magic
# polluting your class and don't need to define valid state transitions,
# you just want some hooks
#
module Basic
def self.included(klass)
klass.extend(ClassMethods)
end
def on(attr, state, &cb)
_cb[[attr, state]] << cb
end
private
def _cb; @_cb ||= Hash.new {|h,k| h[k]=[]}; end
def _trigger_callbacks_for(attr, new_state)
_cb[[attr, new_state]].all? { |cb|
cb.call self.send(attr), new_state
}
end
module ClassMethods
def attr_watch(*attrs)
attrs.each do |attr|
watched_attributes << attr
attr_reader attr
define_method "#{attr}=" do |value|
if _trigger_callbacks_for(attr, value)
instance_variable_set("@#{attr}", value)
else
raise StateMachine::FailedTransition,
"from #{self.send(attr)} to #{value}"
end
end
end
end
def watched_attributes; @watched_attributes ||= []; end
end
end
end
puts 'Basic usage', '----------------'
class Foo
include StateMachine::Basic
# trigger for state changes
attr_watch :state
end
f = Foo.new
f.on(:state, :init) { |fr, to| puts "callback state #{fr} -> #{to}"; true }
f.on(:state, :fail) { |fr, to| puts "callback state #{fr} -> #{to}"; true }
# Note any callback returning false or nil raises FailedTransition
f.on(:state, :fail) { false }
f.state = :init
begin; f.state = :fail; rescue; puts "error tripped: #{$!.class} #{$!}"; end
puts 'Full-featured usage', '----------------'
class Bar
include StateMachine
# trigger for state changes
attr_watch :foo
# define valid transitions
def_state :foo, :init
def_state :foo, :init => [:fail, :succeed]
def initialize(state)
self.foo = state
end
end
b = Bar.new(:init)
b.on_succeed(:foo) { |fr, to| puts "callback state #{fr} -> #{to}"; true }
b.on_fail(:foo) { |fr, to| puts "callback state #{fr} -> #{to}"; true }
# no error: valid transition from init -> succeed
b.succeed!(:foo)
# error: illegal transition from succeed -> fail
begin; b.fail!(:foo); rescue; puts "error tripped: #{$!.class} #{$!}"; end
b2 = Bar.new(:init)
b2.on_succeed(:foo) { |fr, to| puts "callback state #{fr} -> #{to}"; true }
b2.on_fail(:foo) { |fr, to| puts "callback state #{fr} -> #{to}"; true }
# no error: valid transition from init -> fail
b2.fail!(:foo)
# error: illegal transition from fail -> succeed
begin; b2.succeed!(:foo); rescue; puts "error tripped: #{$!.class} #{$!}"; end
# error: illegal transition in constructor
begin; bad = Bar.new(:succeed); rescue; puts "error tripped: #{$!.class} #{$!}"; end
puts 'Injectable procs', '----------------'
# Not that you'd ever want to do this, but to illustrate the point of the
# exercise: how to pass multiple procs into a method (well, into a slightly
# enhanced proc)
InjectableProc = Class.new(Proc) do
include StateMachine::Basic
attr_watch :state
attr_accessor :output
end
p = InjectableProc.new { |input|
p.output = input
p.state = 0
p.output = p.output.upcase
p.state = 1
p.output = p.output.gsub(/HELLO/i,'GOODBYE')
p.state = 2
p.output = p.output.tr('O','X')
p.state = 3
p.output
}
debug = lambda { |fr, to|
puts "callback state #{fr} -> #{to} current output = #{p.output}"; true
}
# reverse the output on every state change
# why? because we can.
(0..3).each do |st|
p.on(:state, st, &debug)
p.on(:state, st) { p.output = p.output.reverse }
end
puts p["Hello world"]
# Using statemachine class to implement Jeremy Evan's example `a` method
# this doesn't quite work due to not accepting multiple params or block params in my callback DSL
# but that could be easily fixed
def a(flag, trigger)
if flag
trigger.true!(:flag, 1, "two"){|b| [b, :foo]}
else
trigger.false!(:flag)
end
end
class Trigger
include StateMachine
attr_watch :flag
def_state :flag, :true
def_state :flag, :false
end
trigger = Trigger.new
trigger.on_true(:flag) {|arg1, arg2, &b| [true, arg1, arg2, b.call(:three)]}
trigger.on_false(:flag) {false}
puts a(true, trigger)
@JEG2
Copy link

JEG2 commented Oct 17, 2011

Cool use of state machines here.

@elight
Copy link

elight commented Oct 17, 2011

Perhaps I'm too stupid to follow this one. My brain hurts. http://www.youtube.com/watch?v=IIlKiRPSNGA

@jeremyevans
Copy link

I'm not sure if I'm missing something, but I don't see how this is passing multiple blocks to a method. Maybe I got lost in all of the StateMachine stuff?

@ericgj
Copy link
Author

ericgj commented Oct 18, 2011

My examples are not super illuminating.... I added one that shows in principle how it would work to do the equivalent of passing multiple blocks to a method. Basically it's a statemachine object that handles the multiple blocks rather than a 'multiblock' of some variety or other.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment