pattern-matching 0.3.0 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
data/lib/behavior.rb ADDED
@@ -0,0 +1,59 @@
1
+ def behavior_info(name, callbacks = {})
2
+ $__behavior_info__ ||= {}
3
+ $__behavior_info__[name.to_sym] = callbacks.inject({}){|memo,(k,v)| memo[k.to_sym] = v; memo}
4
+ end
5
+
6
+ alias :behaviour_info :behavior_info
7
+ alias :interface :behavior_info
8
+
9
+ class Object
10
+ def behaves_as?(name)
11
+
12
+ name = name.to_sym
13
+ bi = $__behavior_info__[name]
14
+ return false if bi.nil?
15
+
16
+ bi.each do |method, arity|
17
+ begin
18
+ return false unless arity == :any || self.method(method).arity == arity
19
+ rescue NameError
20
+ return false
21
+ end
22
+ end
23
+
24
+ return true
25
+ end
26
+ end
27
+
28
+ def behavior(name)
29
+
30
+ name = name.to_sym
31
+ raise ArgumentError.new("undefined behavior '#{name}'") if $__behavior_info__[name].nil?
32
+
33
+ clazz = self.method(:behavior).receiver
34
+
35
+ unless clazz.instance_methods(false).include?(:behaviors)
36
+ class << clazz
37
+ def behaviors
38
+ @behaviors ||= []
39
+ end
40
+ end
41
+ end
42
+
43
+ clazz.behaviors << name
44
+
45
+ class << clazz
46
+ def new(*args, &block)
47
+ name = self.behaviors.first
48
+ obj = super
49
+ unless obj.behaves_as?(name)
50
+ raise ArgumentError.new("undefined callback functions in #{self} (behavior '#{name}')")
51
+ else
52
+ return obj
53
+ end
54
+ end
55
+ end
56
+ end
57
+
58
+ alias :behaviour :behavior
59
+ alias :behaves_as :behavior
data/lib/behaviour.rb ADDED
@@ -0,0 +1,2 @@
1
+ # Support the Scandinavian spelling
2
+ require_relative 'behavior'
@@ -1,143 +1,139 @@
1
- require 'pp'
2
-
3
- module PatternMatching
4
-
5
- VERSION = '0.3.0'
6
-
7
- UNBOUND = Class.new
8
- ALL = Class.new
9
-
10
- private
11
-
12
- class Guard # :nodoc:
13
- def initialize(func, clazz, matcher) # :nodoc:
14
- @func = func
15
- @clazz = clazz
16
- @matcher = matcher
17
- end
18
- def when(&block) # :nodoc:
19
- unless block_given?
20
- raise ArgumentError.new("block missing for `when` guard on function `#{@func}` of class #{@clazz}")
21
- end
22
- @matcher[@matcher.length-1] = block
23
- return nil
24
- end
25
- end
26
-
27
- def self.__match_pattern__(args, pattern) # :nodoc:
28
- return unless (pattern.last == ALL && args.length >= pattern.length) \
29
- || (args.length == pattern.length)
30
- pattern.each_with_index do |p, i|
31
- break if p == ALL && i+1 == pattern.length
32
- arg = args[i]
33
- next if p.is_a?(Class) && arg.is_a?(p)
34
- if p.is_a?(Hash) && arg.is_a?(Hash) && ! p.empty?
35
- p.each do |key, value|
36
- return false unless arg.has_key?(key)
37
- next if value == UNBOUND
38
- return false unless arg[key] == value
39
- end
40
- next
41
- end
42
- return false unless p == UNBOUND || p == arg
43
- end
44
- return true
45
- end
46
-
47
- def self.__unbound_args__(match, args) # :nodoc:
48
- argv = []
49
- match.first.each_with_index do |p, i|
50
- if p == ALL && i == match.first.length-1
51
- argv << args[(i..args.length)].reduce([]){|memo, arg| memo << arg }
52
- elsif p.is_a?(Hash) && p.values.include?(UNBOUND)
53
- p.each do |key, value|
54
- argv << args[i][key] if value == UNBOUND
55
- end
56
- elsif p.is_a?(Hash) || p == UNBOUND || p.is_a?(Class)
57
- argv << args[i]
58
- end
59
- end
60
- return argv
61
- end
62
-
63
- def self.__pattern_match__(clazz, func, *args, &block) # :nodoc:
64
- args = args.first
65
-
66
- # get the array of matchers for this function
67
- matchers = clazz.__function_pattern_matches__[func]
68
- return [:nodef, nil] if matchers.nil?
69
-
70
- # scan through all patterns for this function
71
- index = matchers.index do |matcher|
72
- if PatternMatching.__match_pattern__(args, matcher.first)
73
- if matcher.last.nil?
74
- true # no guard clause
75
- else
76
- self.instance_exec(*PatternMatching.__unbound_args__(matcher, args), &matcher.last)
77
- end
78
- end
79
- end
80
-
81
- if index.nil?
82
- return [:nomatch, nil]
83
- else
84
- return [:ok, matchers[index]]
85
- end
86
- end
87
-
88
- protected
89
-
90
- def self.included(base)
91
-
92
- class << base
93
-
94
- public
95
-
96
- def _() # :nodoc:
97
- return UNBOUND
98
- end
99
-
100
- def defn(func, *args, &block)
101
-
102
- block = Proc.new{} unless block_given?
103
- pattern = __add_pattern_for__(func, *args, &block)
104
-
105
- unless self.instance_methods(false).include?(func)
106
-
107
- define_method(func) do |*args, &block|
108
- result, match = PatternMatching.__pattern_match__(self.method(func).owner, func, args, block)
109
- if result == :ok
110
- # if a match is found call the block
111
- argv = PatternMatching.__unbound_args__(match, args)
112
- return self.instance_exec(*argv, &match[1])
113
- elsif result == :nodef
114
- super(*args, &block)
115
- else
116
- begin
117
- super(*args, &block)
118
- rescue NoMethodError, ArgumentError
119
- raise NoMethodError.new("no method `#{func}` matching #{args} found for class #{self.class}")
120
- end
121
- end
122
- end
123
- end
124
-
125
- return PatternMatching::Guard.new(func, self, pattern)
126
- end
127
-
128
- public
129
-
130
- def __function_pattern_matches__ # :nodoc:
131
- @__function_pattern_matches__ ||= Hash.new
132
- end
133
-
134
- def __add_pattern_for__(func, *args, &block) # :nodoc:
135
- block = Proc.new{} unless block_given?
136
- matchers = self.__function_pattern_matches__
137
- matchers[func] = [] unless matchers.has_key?(func)
138
- matchers[func] << [args, block, nil]
139
- return matchers[func].last
140
- end
141
- end
142
- end
143
- end
1
+ module PatternMatching
2
+
3
+ VERSION = '0.4.0'
4
+
5
+ UNBOUND = Class.new
6
+ ALL = Class.new
7
+
8
+ private
9
+
10
+ GUARD_CLAUSE = Class.new do # :nodoc:
11
+ def initialize(func, clazz, matcher) # :nodoc:
12
+ @func = func
13
+ @clazz = clazz
14
+ @matcher = matcher
15
+ end
16
+ def when(&block) # :nodoc:
17
+ unless block_given?
18
+ raise ArgumentError.new("block missing for `when` guard on function `#{@func}` of class #{@clazz}")
19
+ end
20
+ @matcher[@matcher.length-1] = block
21
+ return nil
22
+ end
23
+ end
24
+
25
+ def self.__match_pattern__(args, pattern) # :nodoc:
26
+ return unless (pattern.last == ALL && args.length >= pattern.length) \
27
+ || (args.length == pattern.length)
28
+ pattern.each_with_index do |p, i|
29
+ break if p == ALL && i+1 == pattern.length
30
+ arg = args[i]
31
+ next if p.is_a?(Class) && arg.is_a?(p)
32
+ if p.is_a?(Hash) && arg.is_a?(Hash) && ! p.empty?
33
+ p.each do |key, value|
34
+ return false unless arg.has_key?(key)
35
+ next if value == UNBOUND
36
+ return false unless arg[key] == value
37
+ end
38
+ next
39
+ end
40
+ return false unless p == UNBOUND || p == arg
41
+ end
42
+ return true
43
+ end
44
+
45
+ def self.__unbound_args__(match, args) # :nodoc:
46
+ argv = []
47
+ match.first.each_with_index do |p, i|
48
+ if p == ALL && i == match.first.length-1
49
+ argv << args[(i..args.length)].reduce([]){|memo, arg| memo << arg }
50
+ elsif p.is_a?(Hash) && p.values.include?(UNBOUND)
51
+ p.each do |key, value|
52
+ argv << args[i][key] if value == UNBOUND
53
+ end
54
+ elsif p.is_a?(Hash) || p == UNBOUND || p.is_a?(Class)
55
+ argv << args[i]
56
+ end
57
+ end
58
+ return argv
59
+ end
60
+
61
+ def self.__pattern_match__(clazz, func, *args, &block) # :nodoc:
62
+ args = args.first
63
+
64
+ matchers = clazz.__function_pattern_matches__[func]
65
+ return [:nodef, nil] if matchers.nil?
66
+
67
+ index = matchers.index do |matcher|
68
+ if PatternMatching.__match_pattern__(args, matcher.first)
69
+ if matcher.last.nil?
70
+ true # no guard clause
71
+ else
72
+ self.instance_exec(*PatternMatching.__unbound_args__(matcher, args), &matcher.last)
73
+ end
74
+ end
75
+ end
76
+
77
+ if index.nil?
78
+ return [:nomatch, nil]
79
+ else
80
+ return [:ok, matchers[index]]
81
+ end
82
+ end
83
+
84
+ protected
85
+
86
+ def self.included(base)
87
+
88
+ class << base
89
+
90
+ public
91
+
92
+ def _() # :nodoc:
93
+ return UNBOUND
94
+ end
95
+
96
+ def defn(func, *args, &block)
97
+ unless block_given?
98
+ raise ArgumentError.new("block missing for definition of function `#{func}` on class #{self}")
99
+ end
100
+
101
+ pattern = __add_pattern_for__(func, *args, &block)
102
+
103
+ unless self.instance_methods(false).include?(func)
104
+
105
+ define_method(func) do |*args, &block|
106
+ result, match = PatternMatching.__pattern_match__(self.method(func).owner, func, args, block)
107
+ if result == :ok
108
+ # if a match is found call the block
109
+ argv = PatternMatching.__unbound_args__(match, args)
110
+ return self.instance_exec(*argv, &match[1])
111
+ else # if result == :nodef || result == :nomatch
112
+ begin
113
+ super(*args, &block)
114
+ rescue NoMethodError, ArgumentError
115
+ raise NoMethodError.new("no method `#{func}` matching #{args} found for class #{self.class}")
116
+ end
117
+ end
118
+ end
119
+ end
120
+
121
+ return GUARD_CLAUSE.new(func, self, pattern)
122
+ end
123
+
124
+ public
125
+
126
+ def __function_pattern_matches__ # :nodoc:
127
+ @__function_pattern_matches__ ||= Hash.new
128
+ end
129
+
130
+ def __add_pattern_for__(func, *args, &block) # :nodoc:
131
+ block = Proc.new{} unless block_given?
132
+ matchers = self.__function_pattern_matches__
133
+ matchers[func] = [] unless matchers.has_key?(func)
134
+ matchers[func] << [args, block, nil]
135
+ return matchers[func].last
136
+ end
137
+ end
138
+ end
139
+ end
@@ -0,0 +1,239 @@
1
+ require 'spec_helper'
2
+
3
+ #behaviour_info(:gen_foo, foo: 0, bar: 1, baz: 2, boom: -1, bam: :any)
4
+
5
+ #class Foo
6
+ #behavior(:gen_foo)
7
+
8
+ #def foo
9
+ #return 'foo/0'
10
+ #end
11
+
12
+ #def bar(one, &block)
13
+ #return 'bar/1'
14
+ #end
15
+
16
+ #def baz(one, two)
17
+ #return 'baz/2'
18
+ #end
19
+
20
+ #def boom(*args)
21
+ #return 'boom/-1'
22
+ #end
23
+
24
+ #def bam
25
+ #return 'bam!'
26
+ #end
27
+ #end
28
+
29
+
30
+
31
+ describe 'behavior/interface definitions' do
32
+
33
+ before(:each) do
34
+ $__behavior_info__ = {}
35
+ end
36
+
37
+ context 'behavior_info/2' do
38
+
39
+ it 'accepts a symbol name' do
40
+ behavior_info(:gen_foo, foo: 0)
41
+ $__behavior_info__.keys.first.should eq :gen_foo
42
+ end
43
+
44
+ it 'accepts a string name' do
45
+ behavior_info('gen_foo', foo: 0)
46
+ $__behavior_info__.keys.first.should eq :gen_foo
47
+ end
48
+
49
+ it 'accepts zero function names' do
50
+ behavior_info(:gen_foo)
51
+ $__behavior_info__.keys.first.should eq :gen_foo
52
+ end
53
+
54
+ it 'accepts symbols for function names' do
55
+ behavior_info(:gen_foo, foo: 0)
56
+ $__behavior_info__.values.first.should == {foo: 0}
57
+ end
58
+
59
+ it 'accepts strings as function names' do
60
+ behavior_info(:gen_foo, 'foo' => 0)
61
+ $__behavior_info__.values.first.should == {foo: 0}
62
+ end
63
+
64
+ it 'accepts numeric arity values' do
65
+ behavior_info(:gen_foo, foo: 0)
66
+ $__behavior_info__.values.first.should == {foo: 0}
67
+ end
68
+
69
+ it 'accepts :any as an arity value' do
70
+ behavior_info(:gen_foo, foo: :any)
71
+ $__behavior_info__.values.first.should == {foo: :any}
72
+ end
73
+ end
74
+
75
+ context 'behavior/1' do
76
+
77
+ it 'raises an exception if the behavior has not been defined' do
78
+ lambda {
79
+ Class.new{
80
+ behavior(:gen_foo)
81
+ }
82
+ }.should raise_error(ArgumentError)
83
+ end
84
+
85
+ it 'can be called multiple times for one class' do
86
+ behavior_info(:gen_foo, foo: 0)
87
+ behavior_info(:gen_bar, bar: 0)
88
+
89
+ lambda {
90
+ Class.new{
91
+ behavior(:gen_foo)
92
+ behavior(:gen_bar)
93
+ }
94
+ }.should_not raise_error
95
+ end
96
+ end
97
+
98
+ context 'behavior check on object creation' do
99
+
100
+ it 'raises an exception when one or more function definitions are missing' do
101
+ behavior_info(:gen_foo, foo: 0, bar: 1)
102
+ clazz = Class.new {
103
+ behavior(:gen_foo)
104
+ def foo() nil; end
105
+ }
106
+
107
+ lambda {
108
+ clazz.new
109
+ }.should raise_error(ArgumentError)
110
+ end
111
+
112
+ it 'raises an exception when one or more functions do not have proper arity' do
113
+ behavior_info(:gen_foo, foo: 0)
114
+ clazz = Class.new {
115
+ behavior(:gen_foo)
116
+ def foo(broken) nil; end
117
+ }
118
+
119
+ lambda {
120
+ clazz.new
121
+ }.should raise_error(ArgumentError)
122
+ end
123
+
124
+ it 'accepts any arity when function arity is set to :any' do
125
+ behavior_info(:gen_foo, foo: :any)
126
+ clazz = Class.new {
127
+ behavior(:gen_foo)
128
+ def foo(first) nil; end
129
+ }
130
+
131
+ lambda {
132
+ clazz.new
133
+ }.should_not raise_error(ArgumentError)
134
+ end
135
+
136
+ it 'creates the object when function definitions match' do
137
+ behavior_info(:gen_foo, foo: 0, bar: 1)
138
+ clazz = Class.new {
139
+ behavior(:gen_foo)
140
+ def foo() nil; end
141
+ def bar(first) nil; end
142
+ }
143
+
144
+ lambda {
145
+ clazz.new
146
+ }.should_not raise_error(ArgumentError)
147
+ end
148
+ end
149
+
150
+ context '#behaves_as?' do
151
+
152
+ it 'returns true when the behavior is fully suported' do
153
+ behavior_info(:gen_foo, foo: 0, bar: 1, baz: 2)
154
+ clazz = Class.new {
155
+ def foo() nil; end
156
+ def bar(first) nil; end
157
+ def baz(first, second) nil; end
158
+ }
159
+
160
+ clazz.new.behaves_as?(:gen_foo).should be_true
161
+ end
162
+
163
+ it 'accepts any arity when function arity is set to :any' do
164
+ behavior_info(:gen_foo, foo: :any)
165
+ clazz = Class.new {
166
+ def foo(*args, &block) nil; end
167
+ }
168
+
169
+ clazz.new.behaves_as?(:gen_foo).should be_true
170
+ end
171
+
172
+ it 'returns false when the behavior is partially supported' do
173
+ behavior_info(:gen_foo, foo: 0, bar: 1, baz: 2)
174
+ clazz = Class.new {
175
+ def foo() nil; end
176
+ def bar(first) nil; end
177
+ }
178
+
179
+ clazz.new.behaves_as?(:gen_foo).should be_false
180
+ end
181
+
182
+ it 'returns false when the behavior is not supported at all' do
183
+ behavior_info(:gen_foo, foo: 0, bar: 1, baz: 2)
184
+ clazz = Class.new { }
185
+ clazz.new.behaves_as?(:gen_foo).should be_false
186
+ end
187
+
188
+ it 'returns false when the behavior does not exist' do
189
+ clazz = Class.new { }
190
+ clazz.new.behaves_as?(:gen_foo).should be_false
191
+ end
192
+
193
+ it 'accepts behavior name as a symbol' do
194
+ behavior_info(:gen_foo)
195
+ clazz = Class.new { }
196
+ clazz.new.behaves_as?(:gen_foo).should be_true
197
+ end
198
+
199
+ it 'accepts behavior name as a string' do
200
+ behavior_info(:gen_foo)
201
+ clazz = Class.new { }
202
+ clazz.new.behaves_as?('gen_foo').should be_true
203
+ end
204
+ end
205
+
206
+ context 'aliases' do
207
+
208
+ it 'aliases behaviour_info for behavior_info' do
209
+ behaviour_info(:gen_foo)
210
+ clazz = Class.new { }
211
+ clazz.new.behaves_as?(:gen_foo).should be_true
212
+ end
213
+
214
+ it 'aliases interface for behavior_info' do
215
+ interface(:gen_foo)
216
+ clazz = Class.new { }
217
+ clazz.new.behaves_as?(:gen_foo).should be_true
218
+ end
219
+
220
+ it 'aliases behaviour for behavior' do
221
+ behavior_info(:gen_foo, foo: 0)
222
+ clazz = Class.new {
223
+ behaviour(:gen_foo)
224
+ def foo() nil; end
225
+ }
226
+ clazz.new.behaves_as?(:gen_foo).should be_true
227
+ end
228
+
229
+ it 'aliases behaves_as for behavior' do
230
+ behavior_info(:gen_foo, foo: 0)
231
+ clazz = Class.new {
232
+ behaves_as :gen_foo
233
+ def foo() nil; end
234
+ }
235
+ clazz.new.behaves_as?(:gen_foo).should be_true
236
+ end
237
+ end
238
+
239
+ end