diff --git a/lib/multi_methods.rb b/lib/multi_methods.rb index 59a6a85..9ca4543 100644 --- a/lib/multi_methods.rb +++ b/lib/multi_methods.rb @@ -10,41 +10,48 @@ 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 @@ -52,9 +59,12 @@ def defmethod method_name, 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 diff --git a/specs/multi_methods_spec.rb b/specs/multi_methods_spec.rb index 47bf35e..277a5ca 100644 --- a/specs/multi_methods_spec.rb +++ b/specs/multi_methods_spec.rb @@ -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 @@ -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 @@ -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) @@ -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 } @@ -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} @@ -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 @@ -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 @@ -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