pattern-matching 0.3.0 → 0.4.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/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