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.
- data/lib/multi_methods.rb +77 -0
- data/specs/multi_methods_spec.rb +209 -0
- data/specs/square.rb +25 -0
- metadata +72 -0
@@ -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
|
+
|