multi_methods.rb 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
+