multi_methods.rb 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,77 @@
1
+ module MultiMethods
2
+ def self.included base
3
+ base.extend( ClassMethods )
4
+ base.class_eval { include InstanceMethods }
5
+ end
6
+
7
+ module ClassMethods
8
+
9
+ def create_method( name, &block )
10
+ self.send( :define_method, name, block )
11
+ end
12
+
13
+ def defmulti method_name, default_dispatch_fn = nil
14
+ self.instance_variable_set( "@" + method_name.to_s, [] )
15
+
16
+ create_method( method_name ) do |*args|
17
+ dispatch_table = self.class.instance_variable_get( "@" + method_name.to_s )
18
+
19
+ dispatch_table.each do |m|
20
+ predicate = if m.keys.first.respond_to? :call
21
+ raise "Dispatch method already defined by defmulti" if default_dispatch_fn
22
+ m.keys.first
23
+ elsif m.keys.first == :default
24
+ :default
25
+ else
26
+ lambda { |args| return default_dispatch_fn.call(args) == m.keys.first }
27
+ end
28
+
29
+ destination_fn = m.values.first
30
+
31
+ if predicate == :default || predicate.call(args)
32
+ if destination_fn.is_a? UnboundMethod
33
+ break destination_fn.bind( self ).call(args)
34
+ else
35
+ break destination_fn.call(args)
36
+ end
37
+ break
38
+ end
39
+ raise "No matching dispatcher function found" if dispatch_table.last == m
40
+ end
41
+ end
42
+ end
43
+
44
+ def defmethod method_name, dispatch_value, default_dispatch_fn
45
+ multi_method = self.instance_variable_get( "@" + method_name.to_s)
46
+ raise "MultiMethod #{method_name} not defined" unless multi_method
47
+ multi_method << { dispatch_value => default_dispatch_fn }
48
+ end
49
+ end #ClassMethods
50
+
51
+
52
+ module InstanceMethods
53
+
54
+ def defmulti_local &block
55
+ instance_eval &block
56
+
57
+ #clean up after evaling block
58
+ instance_eval do
59
+ method_name = instance_variable_get( :@added_multi_method )
60
+ self.class.send(:undef_method, method_name)
61
+ self.class.send(:remove_instance_variable, ('@' + method_name.to_s).to_sym )
62
+ self.send( :remove_instance_variable, :@added_multi_method )
63
+ end
64
+ end
65
+
66
+ def defmulti method_name, default_dispatch_fn = nil
67
+ instance_variable_set( :@added_multi_method, method_name )
68
+ self.class.defmulti method_name, default_dispatch_fn
69
+ end
70
+
71
+ def defmethod method_name, dispatch_value, default_dispatch_fn
72
+ self.class.defmethod method_name, dispatch_value, default_dispatch_fn
73
+ end
74
+ end #InstanceMethods
75
+ end
76
+
77
+ Object.send( :include, MultiMethods )
@@ -0,0 +1,209 @@
1
+ require File.dirname(__FILE__) + '/square'
2
+ require File.dirname(__FILE__) + '/../multi_methods'
3
+
4
+
5
+ describe "hacking Square with multi_methods" do
6
+
7
+ describe "defmulti_local" do
8
+ before(:each) do
9
+ @our_square.dispatch_fn = nil
10
+ @our_square.class.dispatch_fn = nil
11
+ end
12
+
13
+ before(:all) do
14
+ class Square
15
+
16
+ def tuna1 *args
17
+ @dispatch_fn = :tuna1
18
+ end
19
+
20
+ def tuna_gateway *args
21
+ defmulti_local do
22
+ defmulti :tuna, lambda{ |args| args[0] + args[1] }
23
+ defmethod :tuna, 2, self.class.instance_method(:tuna1)
24
+ defmethod :tuna, 4, self.class.method(:tuna2)
25
+ defmethod :tuna, :default, lambda{ |args| @default_fn = :tuna_default }
26
+
27
+ tuna(*args)
28
+ end
29
+ end
30
+ end
31
+
32
+ @our_square = Square.new
33
+ end
34
+
35
+ it "should dispatch to tuna1 when the sum of the first to parameters is 2" do
36
+ @our_square.tuna_gateway(1,1)
37
+ @our_square.dispatch_fn.should == :tuna1
38
+ @our_square.class.dispatch_fn.should == nil
39
+ end
40
+
41
+ it "should dispatch to tuna2 when the sum of the first to parameters is 4" do
42
+ @our_square.tuna_gateway(3,1)
43
+ @our_square.dispatch_fn.should == nil
44
+ @our_square.class.dispatch_fn.should == :tuna2
45
+ end
46
+
47
+ it "should remove all traces of metaprogramming after the defmulti_local block exits" do
48
+ @our_square.tuna_gateway(3,1)
49
+ @our_square.methods.should_not include 'tuna'
50
+ @our_square.class.instance_variables.should_not include '@tuna'
51
+ @our_square.class.instance_variables.should_not include '@added_multi_method'
52
+ end
53
+ end
54
+
55
+ describe "causes of exceptions" do
56
+ before do
57
+ @our_square = Square.new
58
+ end
59
+ it "should raise an exception if trying to construct a defmethod without a previously defined defmulti of the same name" do
60
+ lambda do
61
+ @our_square.class.instance_eval { defmethod :chicken, lambda{ |args| args[0] }, instance_method(:chicken1) }
62
+ end.should raise_error( Exception, "MultiMethod chicken not defined" )
63
+ end
64
+
65
+ it "should raise an exception if no predicates match and there is no default defmethod" do
66
+ @our_square.class.instance_eval do
67
+ defmulti :chicken, lambda{ |args| args[1].class }
68
+ defmethod :chicken, Fixnum, instance_method(:chicken1)
69
+ defmethod :chicken, String, method(:chicken2)
70
+ end
71
+ lambda { @our_square.chicken( true ) }.should raise_error( Exception, "No matching dispatcher function found" )
72
+ end
73
+
74
+ it "should raise an exception if defining individual dispatch predicates AND a default dispatch fn" do
75
+ @our_square.class.instance_eval do
76
+ defmulti :chicken, lambda{ |args| args[1].class }
77
+ defmethod :chicken, Fixnum, instance_method(:chicken1)
78
+ defmethod :chicken, String, method(:chicken2)
79
+ defmethod :chicken, lambda { |args| true }, lambda { |args| puts "never get here" }
80
+ end
81
+ lambda do
82
+ @our_square.chicken(2)
83
+ end.should raise_error( Exception, "Dispatch method already defined by defmulti" )
84
+ end
85
+
86
+ end
87
+
88
+ describe "class level with a single dispatch fn" do
89
+ describe "dispatch by on type of 2nd arg" do
90
+ before do
91
+ @our_square = Square.new
92
+
93
+ @our_square.class.instance_eval do
94
+ defmulti :chicken, lambda{ |args| args[1].class }
95
+ defmethod :chicken, Fixnum, instance_method(:chicken1)
96
+ defmethod :chicken, String, method(:chicken2)
97
+ defmethod :chicken, :default, lambda { @dispatch_fn = :chicken_default; return :chicken_default }
98
+ end
99
+
100
+ @our_square.dispatch_fn = nil
101
+ @our_square.class.dispatch_fn = nil
102
+ end
103
+
104
+ it "should create an instance method named chicken and a class level instance variable" do
105
+ @our_square.methods.should include 'chicken'
106
+ @our_square.class.instance_variables.should include '@chicken'
107
+ end
108
+
109
+ it "should dispatch to chicken1 if the 2nd arg is a Fixnum" do
110
+ @our_square.chicken( true, 2 )
111
+ @our_square.dispatch_fn.should == :chicken1
112
+ @our_square.class.dispatch_fn.should be_nil
113
+ end
114
+
115
+ it "should dispatch to chicken1 if the 2nd arg is a Fixnum and return the correct value from chicken1" do
116
+ result = @our_square.chicken( true, 2 )
117
+ result.should == :chicken1
118
+ end
119
+
120
+ it "should dispatch to chicken2 if the 2nd arg is a String" do
121
+ @our_square.chicken( true, "two" )
122
+ @our_square.dispatch_fn.should be_nil
123
+ @our_square.class.dispatch_fn.should == :chicken2
124
+ end
125
+
126
+ it "should dispatch to the default lambda if the 2nd arg is neither a Fixnum nor a String and return the correct value" do
127
+ result = @our_square.chicken( true, true )
128
+ @our_square.dispatch_fn.should be_nil
129
+ @our_square.class.dispatch_fn.should == :chicken_default
130
+ result.should == :chicken_default
131
+ end
132
+ end
133
+
134
+ describe "dispatch based on the # of args " do
135
+ before do
136
+ @our_square = Square.new
137
+
138
+ @our_square.class.instance_eval do
139
+ defmulti :chicken, lambda{ |args| args.size }
140
+ defmethod :chicken, 1, instance_method(:chicken1)
141
+ defmethod :chicken, 2, method(:chicken2)
142
+ defmethod :chicken, :default, lambda { @dispatch_fn = :chicken_default}
143
+ end
144
+
145
+ @our_square.dispatch_fn = nil
146
+ @our_square.class.dispatch_fn = nil
147
+ end
148
+
149
+ it "should dispatch to chicken1 if called with one arg" do
150
+ @our_square.chicken(1)
151
+ @our_square.dispatch_fn.should == :chicken1
152
+ @our_square.class.dispatch_fn.should be_nil
153
+ end
154
+
155
+ it "should dispatch to chicken2 if called with two args" do
156
+ @our_square.chicken(1,2)
157
+ @our_square.dispatch_fn.should be_nil
158
+ @our_square.class.dispatch_fn.should == :chicken2
159
+ end
160
+
161
+ it "should dispatch to chicken3 if called with three args" do
162
+ @our_square.chicken(1,2,3)
163
+ @our_square.class.dispatch_fn.should == :chicken_default
164
+ @our_square.dispatch_fn.should be_nil
165
+ end
166
+
167
+ it "should dispatch to chicken3 if called with four args" do
168
+ @our_square.chicken(1,2,3,4)
169
+ @our_square.class.dispatch_fn.should == :chicken_default
170
+ @our_square.dispatch_fn.should be_nil
171
+ end
172
+ end
173
+ end
174
+
175
+ describe "class level with multiple predicates" do
176
+ before do
177
+ @our_square = Square.new
178
+
179
+ @our_square.class.instance_eval do
180
+ defmulti :chicken
181
+ defmethod :chicken, lambda{ |args| args[0].class == Fixnum && args[1].class == Fixnum }, instance_method(:chicken1)
182
+ defmethod :chicken, lambda{ |args| args[0].class == String && args[1].class == String }, method(:chicken2)
183
+ defmethod :chicken, :default, lambda { @dispatch_fn = :chicken_default}
184
+ end
185
+
186
+ @our_square.dispatch_fn = nil
187
+ @our_square.class.dispatch_fn = nil
188
+ end
189
+
190
+ it "should dispatch to chicken1 if the first two parameters are Fixnums" do
191
+ result = @our_square.chicken(1, 2)
192
+ @our_square.dispatch_fn.should == :chicken1
193
+ @our_square.class.dispatch_fn.should be_nil
194
+ end
195
+
196
+ it "should dispatch to chicken2 if the first two parameters are Strings" do
197
+ @our_square.chicken("one", "two")
198
+ @our_square.dispatch_fn.should be_nil
199
+ @our_square.class.dispatch_fn.should == :chicken2
200
+ end
201
+
202
+ it "should dispatch to chicken3 if the first two parameters are neither Fixnums or Strings" do
203
+ @our_square.chicken("one", 2)
204
+ @our_square.dispatch_fn.should be_nil
205
+ @our_square.class.dispatch_fn.should == :chicken_default
206
+ end
207
+ end
208
+
209
+ end
data/specs/square.rb ADDED
@@ -0,0 +1,25 @@
1
+ class Square
2
+ class <<self
3
+ attr_accessor :dispatch_fn
4
+ end
5
+ attr_accessor :dispatch_fn
6
+
7
+ def chicken1 *args
8
+ @dispatch_fn = :chicken1
9
+ return :chicken1
10
+ end
11
+
12
+ def self.chicken2 *args
13
+ @dispatch_fn = :chicken2
14
+ return :chicken2
15
+ end
16
+
17
+ def tuna1 *args
18
+ @dispatch_fn = :tuna1
19
+ end
20
+
21
+ def self.tuna2 *args
22
+ @dispatch_fn = :tuna2
23
+ end
24
+
25
+ end
metadata ADDED
@@ -0,0 +1,72 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: multi_methods.rb
3
+ version: !ruby/object:Gem::Version
4
+ hash: 23
5
+ prerelease:
6
+ segments:
7
+ - 1
8
+ - 0
9
+ - 0
10
+ version: 1.0.0
11
+ platform: ruby
12
+ authors:
13
+ - Paul Santa Clara
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2011-02-05 00:00:00 -05:00
19
+ default_executable:
20
+ dependencies: []
21
+
22
+ description: |
23
+ Supports general dispatch using clojure style multi-methods. This can be used
24
+ for anything from basic function overloading to a function dispatch based on arbitrary complexity.
25
+
26
+ email: kesserich1@gmail.com
27
+ executables: []
28
+
29
+ extensions: []
30
+
31
+ extra_rdoc_files: []
32
+
33
+ files:
34
+ - lib/multi_methods.rb
35
+ - specs/multi_methods_spec.rb
36
+ - specs/square.rb
37
+ has_rdoc: true
38
+ homepage:
39
+ licenses: []
40
+
41
+ post_install_message:
42
+ rdoc_options: []
43
+
44
+ require_paths:
45
+ - lib
46
+ required_ruby_version: !ruby/object:Gem::Requirement
47
+ none: false
48
+ requirements:
49
+ - - ">="
50
+ - !ruby/object:Gem::Version
51
+ hash: 3
52
+ segments:
53
+ - 0
54
+ version: "0"
55
+ required_rubygems_version: !ruby/object:Gem::Requirement
56
+ none: false
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ hash: 3
61
+ segments:
62
+ - 0
63
+ version: "0"
64
+ requirements: []
65
+
66
+ rubyforge_project:
67
+ rubygems_version: 1.5.0
68
+ signing_key:
69
+ specification_version: 3
70
+ summary: General dispatch for ruby
71
+ test_files: []
72
+