Skip to content

Instantly share code, notes, and snippets.

@rbriank
Created May 24, 2012 19:58
Show Gist options
  • Save rbriank/2783857 to your computer and use it in GitHub Desktop.
Save rbriank/2783857 to your computer and use it in GitHub Desktop.
Gem::Specification.new do |s|
s.name = 'multi_methods'
s.version = '1.0.3'
s.platform = Gem::Platform::RUBY
s.author = 'Brian Kierstead'
s.email = 'briankierstead@gmail.com'
s.summary = 'General dispatch for ruby'
s.description = <<EOS
Supports general dispatch using clojure style multi-methods. This can be used
for anything from basic function overloading to a function dispatch based on arbitrary complexity. Based on the work of Andrew Garson (https://github.com/andrewGarson/ruby-multimethods) and Paul Santa Clara (https://github.com/psantacl/ruby-multimethods)
EOS
s.files = ['multi_methods.rb']
s.test_file = 'multi_methods_spec.rb'
s.require_path = '.'
s.add_development_dependency('rspec', ["~> 2.0"])
end
module MultiMethods
def self.included base
base.extend( ClassMethods )
base.class_eval { include InstanceMethods }
end
module ClassMethods
def create_method( name, &block )
self.send( :define_method, name, block )
end
def defmulti method_name, default_dispatch_fn = nil
self.instance_variable_set( "@" + method_name.to_s, [] )
create_method( method_name ) do |*args|
def multimethod_exec callable, args_list
target = (callable.is_a? UnboundMethod) ? callable.bind(self) : callable
arity = target.arity
if arity == 0
target.call
elsif arity > 0
target.call(*args_list[0..arity-1])
elsif arity < 0
target.call(*args_list)
end
end
dispatch_table = self.class.instance_variable_get( "@" + method_name.to_s )
destination_fn = nil
default_fn = nil
default_dispatch_result = multimethod_exec(default_dispatch_fn, args) if default_dispatch_fn
dispatch_table.each do |m|
predicate = if m.keys.first.respond_to? :call
raise "Dispatch method already defined by defmulti" if default_dispatch_fn
m.keys.first
elsif m.keys.first == :default
default_fn = m.values.first
lambda { |*args| false }
else
lambda { |*args| return default_dispatch_result == m.keys.first }
end
destination_fn = m.values.first if multimethod_exec(predicate, args)
end
destination_fn ||= default_fn
raise "No matching dispatcher function found" unless destination_fn
multimethod_exec destination_fn, args
end
end
def defmethod method_name, dispatch_value, default_dispatch_fn
multi_method = self.instance_variable_get( "@" + method_name.to_s)
raise "MultiMethod #{method_name} not defined" unless multi_method
multi_method << { dispatch_value => default_dispatch_fn }
end
end #ClassMethods
module InstanceMethods
def defmulti_dirty &block
instance_eval &block
end
def defmulti_local &block
dispatch_return = instance_eval &block
#clean up after evaling block
instance_eval do
method_name = instance_variable_get( :@added_multi_method )
self.class.send(:undef_method, method_name)
self.class.send(:remove_instance_variable, ('@' + method_name.to_s).to_sym )
self.send( :remove_instance_variable, :@added_multi_method )
end
dispatch_return
end
def defmulti method_name, default_dispatch_fn = nil
instance_variable_set( :@added_multi_method, method_name )
self.class.defmulti method_name, default_dispatch_fn
end
def defmethod method_name, dispatch_value, default_dispatch_fn
self.class.defmethod method_name, dispatch_value, default_dispatch_fn
end
end #InstanceMethods
end
Object.send( :include, MultiMethods )
require File.expand_path('multi_methods')
class Square
class <<self
attr_accessor :dispatch_fn
end
attr_accessor :dispatch_fn
def chicken1 *args
@dispatch_fn = :chicken1
return :chicken1
end
def self.chicken2 *args
@dispatch_fn = :chicken2
return :chicken2
end
def tuna1 *args
@dispatch_fn = :tuna1
end
def self.tuna2 *args
@dispatch_fn = :tuna2
end
end
describe "hacking Square with multi_methods" do
describe "defmulti_local" do
before(:each) do
@our_square.dispatch_fn = nil
@our_square.class.dispatch_fn = nil
end
before(:all) do
class Square
def tuna1 *args
@dispatch_fn = :tuna1
return 69
end
def tuna_gateway *args
defmulti_local do
defmulti :tuna, lambda{ |*args| args[0] + args[1] }
defmethod :tuna, 2, self.class.instance_method(:tuna1)
defmethod :tuna, 4, self.class.method(:tuna2)
defmethod :tuna, :default, lambda{ |*args| @default_fn = :tuna_default }
tuna(*args)
end
end
end
@our_square = Square.new
end
it "should dispatch to tuna1 when the sum of the first to parameters is 2" do
secret = @our_square.tuna_gateway(1,1)
@our_square.dispatch_fn.should == :tuna1
@our_square.class.dispatch_fn.should == nil
end
it "should dispatch to tuna2 when the sum of the first to parameters is 4" do
@our_square.tuna_gateway(3,1)
@our_square.dispatch_fn.should == nil
@our_square.class.dispatch_fn.should == :tuna2
end
it "should remove all traces of metaprogramming after the defmulti_local block exits" do
@our_square.tuna_gateway(3,1)
@our_square.methods.should_not include 'tuna'
@our_square.class.instance_variables.should_not include '@tuna'
@our_square.class.instance_variables.should_not include '@added_multi_method'
end
it "should return the value of whatever fn it dispatched to" do
secret = @our_square.tuna_gateway(1,1)
secret.should == 69
end
end
describe "causes of exceptions" do
before do
@our_square = Square.new
end
it "should raise an exception if trying to construct a defmethod without a previously defined defmulti of the same name" do
lambda do
@our_square.class.instance_eval { defmethod :chicken, lambda{ |*args| args[0] }, instance_method(:chicken1) }
end.should raise_error( Exception, "MultiMethod chicken not defined" )
end
it "should raise an exception if no predicates match and there is no default defmethod" do
@our_square.class.instance_eval do
defmulti :chicken, lambda{ |*args| args[1].class }
defmethod :chicken, Fixnum, instance_method(:chicken1)
defmethod :chicken, String, method(:chicken2)
end
lambda { @our_square.chicken( true ) }.should raise_error( Exception, "No matching dispatcher function found" )
end
it "should raise an exception if defining individual dispatch predicates AND a default dispatch fn" do
@our_square.class.instance_eval do
defmulti :chicken, lambda{ |*args| args[1].class }
defmethod :chicken, Fixnum, instance_method(:chicken1)
defmethod :chicken, String, method(:chicken2)
defmethod :chicken, lambda { |*args| true }, lambda { |*args| puts "never get here" }
end
lambda do
@our_square.chicken(2)
end.should raise_error( Exception, "Dispatch method already defined by defmulti" )
end
end
describe "class level with a single dispatch fn" do
describe "dispatch by on type of 2nd arg" do
before do
@our_square = Square.new
@our_square.class.instance_eval do
defmulti :chicken, lambda{ |*args| args[1].class }
defmethod :chicken, Fixnum, instance_method(:chicken1)
defmethod :chicken, String, method(:chicken2)
defmethod :chicken, :default, lambda { @dispatch_fn = :chicken_default; return :chicken_default }
end
@our_square.dispatch_fn = nil
@our_square.class.dispatch_fn = nil
end
it "should create an instance method named chicken and a class level instance variable" do
@our_square.methods.should include 'chicken'
@our_square.class.instance_variables.should include '@chicken'
end
it "should dispatch to chicken1 if the 2nd arg is a Fixnum" do
@our_square.chicken( true, 2 )
@our_square.dispatch_fn.should == :chicken1
@our_square.class.dispatch_fn.should be_nil
end
it "should dispatch to chicken1 if the 2nd arg is a Fixnum and return the correct value from chicken1" do
result = @our_square.chicken( true, 2 )
result.should == :chicken1
end
it "should dispatch to chicken2 if the 2nd arg is a String" do
@our_square.chicken( true, "two" )
@our_square.dispatch_fn.should be_nil
@our_square.class.dispatch_fn.should == :chicken2
end
it "should dispatch to the default lambda if the 2nd arg is neither a Fixnum nor a String and return the correct value" do
result = @our_square.chicken( true, true )
@our_square.dispatch_fn.should be_nil
@our_square.class.dispatch_fn.should == :chicken_default
result.should == :chicken_default
end
end
describe "dispatch based on the # of args " do
before do
@our_square = Square.new
@our_square.class.instance_eval do
defmulti :chicken, lambda{ |*args| args.size }
defmethod :chicken, 1, instance_method(:chicken1)
defmethod :chicken, 2, method(:chicken2)
defmethod :chicken, :default, lambda { @dispatch_fn = :chicken_default}
end
@our_square.dispatch_fn = nil
@our_square.class.dispatch_fn = nil
end
it "should dispatch to chicken1 if called with one arg" do
@our_square.chicken(1)
@our_square.dispatch_fn.should == :chicken1
@our_square.class.dispatch_fn.should be_nil
end
it "should dispatch to chicken2 if called with two args" do
@our_square.chicken(1,2)
@our_square.dispatch_fn.should be_nil
@our_square.class.dispatch_fn.should == :chicken2
end
it "should dispatch to chicken3 if called with three args" do
@our_square.chicken(1,2,3)
@our_square.class.dispatch_fn.should == :chicken_default
@our_square.dispatch_fn.should be_nil
end
it "should dispatch to chicken3 if called with four args" do
@our_square.chicken(1,2,3,4)
@our_square.class.dispatch_fn.should == :chicken_default
@our_square.dispatch_fn.should be_nil
end
end
end
describe "class level with multiple predicates" do
before do
@our_square = Square.new
@our_square.class.instance_eval do
defmulti :chicken
defmethod :chicken, lambda{ |*args| args[0].class == Fixnum && args[1].class == Fixnum }, instance_method(:chicken1)
defmethod :chicken, lambda{ |*args| args[0].class == String && args[1].class == String }, method(:chicken2)
defmethod :chicken, :default, lambda { @dispatch_fn = :chicken_default}
end
@our_square.dispatch_fn = nil
@our_square.class.dispatch_fn = nil
end
it "should dispatch to chicken1 if the first two parameters are Fixnums" do
result = @our_square.chicken(1, 2)
@our_square.dispatch_fn.should == :chicken1
@our_square.class.dispatch_fn.should be_nil
end
it "should dispatch to chicken2 if the first two parameters are Strings" do
@our_square.chicken("one", "two")
@our_square.dispatch_fn.should be_nil
@our_square.class.dispatch_fn.should == :chicken2
end
it "should dispatch to chicken3 if the first two parameters are neither Fixnums or Strings" do
@our_square.chicken("one", 2)
@our_square.dispatch_fn.should be_nil
@our_square.class.dispatch_fn.should == :chicken_default
end
end
describe "default method" do
it "should only be called if no other methods match" do
@our_square = Square.new
@our_square.class.instance_eval do
defmulti :puppy, lambda { |*args| args[0] }
defmethod :puppy, 1, lambda { |*args| :one }
defmethod :puppy, :default, lambda { |*args| :default }
defmethod :puppy, 2, lambda { |*args| :two }
end
result = @our_square.puppy 2
result.should == :two
end
end
describe "arg splatting" do
before(:all) do
@our_square = Square.new
@our_square.class.instance_eval do
def glorb a, b=nil
[a, b]
end
defmulti :kitten, lambda { |a| a }
defmethod :kitten, 1, lambda { |a, b| [a,b] }
defmethod :kitten, 2, lambda { |a, b, c| [a,b,c] }
defmethod :kitten, 3, lambda { |a, b, c, d| [a,b,c,d] }
defmethod :kitten, 4, lambda { |*args| args.reverse }
defmethod :kitten, 5, method(:glorb)
defmethod :kitten, 6, lambda { |a, *rest| [a, rest] }
defmethod :kitten, :default, lambda { |*args| args }
end
end
it "should pass the number of args the lambda is expecting when it doesn't want a splatted list" do
@our_square.kitten(1,:b,:c,:d,:e,:f,:g).should == [1, :b]
@our_square.kitten(2,:b,nil,:d,:e,:f,:g).should == [2, :b, nil]
@our_square.kitten(3,:b,nil,[:d],:e,:f,:g).should == [3, :b, nil, [:d]]
@our_square.kitten(:guava,:b,:c,:d,:e,:f,:g).should == [:guava, :b, :c, :d, :e, :f, :g]
@our_square.kitten(5).should == [5, nil]
@our_square.kitten(5, :b).should == [5, :b]
end
it "should raise an argument exception if there are not enough arguments to satisfy the required args for a dispatch_fn" do
lambda { @our_square.kitten(1) }.should raise_error( Exception, "wrong number of arguments (1 for 2)" )
end
it "should not raise an argument exception if there are not enough arguments to satisfy optional args for a dispatch_fn" do
lambda { @our_square.kitten(5) }.should_not raise_error( Exception, "wrong number of arguments (1 for 2)" )
end
it "should raise an argument exception if there are too many arguments for a method" do
#NOTE: not sure why the exception is saying (3 for 1), :glorb takes 2 arguments, second is optional
lambda { @our_square.kitten(5,:b,:c) }.should raise_error(Exception, "wrong number of arguments (3 for 1)")
end
it "should pass the entire arg array when the lambda is expecting one splatted arg" do
@our_square.kitten(4,:b,:c,:d,:e,:f,:g).should == [4,:b,:c,:d,:e,:f,:g].reverse
end
it "should pass the correct number of args plus rest in the splatted args list when the dispatch_fn takes multiple args and a splatted arg" do
@our_square.kitten(6).should == [6,[]]
@our_square.kitten(6,:b,:c).should == [6, [:b, :c]]
end
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment