From cd70a78a6608591d493849e19c45f564aef2f788 Mon Sep 17 00:00:00 2001 From: unknown Date: Wed, 18 May 2011 13:20:32 -0400 Subject: [PATCH] added ability to name arguments for defmulti and defmethod lambdas (predicates and targets). If you want a single varargs list contains all of the args, you must splat it like any other function. Handles optional args on methods used as dispatch targets. Should handle varargs used as not the last parameter to a method, as is allowed in 1.9.2, but I am not sure how to test that and still have the tests runnable under 1.8.7. --- lib/multi_methods.rb | 34 ++++++++++------ specs/multi_methods_spec.rb | 79 ++++++++++++++++++++++++++++++------- 2 files changed, 87 insertions(+), 26 deletions(-) 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