Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 22 additions & 12 deletions lib/multi_methods.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,51 +10,61 @@ 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 = default_dispatch_fn.call(args) if default_dispatch_fn
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 }
lambda { |*args| false }
else
lambda { |args| return default_dispatch_result == m.keys.first }
lambda { |*args| return default_dispatch_result == m.keys.first }
end

destination_fn = m.values.first if predicate.call(args)
destination_fn = m.values.first if multimethod_exec(predicate, args)
end

destination_fn ||= default_fn
raise "No matching dispatcher function found" unless destination_fn

if destination_fn.is_a? UnboundMethod
destination_fn.bind(self).call(args)
else
destination_fn.call(args)
end

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

Expand Down
79 changes: 65 additions & 14 deletions specs/multi_methods_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,10 @@ def tuna1 *args

def tuna_gateway *args
defmulti_local do
defmulti :tuna, lambda{ |args| args[0] + args[1] }
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 }
defmethod :tuna, :default, lambda{ |*args| @default_fn = :tuna_default }

tuna(*args)
end
Expand Down Expand Up @@ -64,13 +64,13 @@ def tuna_gateway *args
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) }
@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 }
defmulti :chicken, lambda{ |*args| args[1].class }
defmethod :chicken, Fixnum, instance_method(:chicken1)
defmethod :chicken, String, method(:chicken2)
end
Expand All @@ -79,10 +79,10 @@ def tuna_gateway *args

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 }
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" }
defmethod :chicken, lambda { |*args| true }, lambda { |*args| puts "never get here" }
end
lambda do
@our_square.chicken(2)
Expand All @@ -97,7 +97,7 @@ def tuna_gateway *args
@our_square = Square.new

@our_square.class.instance_eval do
defmulti :chicken, lambda{ |args| args[1].class }
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 }
Expand Down Expand Up @@ -142,7 +142,7 @@ def tuna_gateway *args
@our_square = Square.new

@our_square.class.instance_eval do
defmulti :chicken, lambda{ |args| args.size }
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}
Expand Down Expand Up @@ -184,8 +184,8 @@ def tuna_gateway *args

@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, 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

Expand Down Expand Up @@ -216,10 +216,10 @@ def tuna_gateway *args
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 }
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
Expand All @@ -228,4 +228,55 @@ def tuna_gateway *args
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