pattern-matching 0.2.0 → 0.3.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.
- checksums.yaml +8 -8
- data/README.md +33 -7
- data/lib/pattern_matching.rb +90 -80
- data/spec/integration_spec.rb +46 -11
- data/spec/pattern_matching_spec.rb +17 -2
- metadata +3 -3
checksums.yaml
CHANGED
@@ -1,15 +1,15 @@
|
|
1
1
|
---
|
2
2
|
!binary "U0hBMQ==":
|
3
3
|
metadata.gz: !binary |-
|
4
|
-
|
4
|
+
NTczMjQ3ODhmZmM1ZTFjYWI0NDc3Mjk4ZDg0ZWNiZTI2MTEyMWUzNg==
|
5
5
|
data.tar.gz: !binary |-
|
6
|
-
|
6
|
+
YzQwN2I4N2VhNDhlOGJjZjM0MmZjNGI1OTFkZDE1NWNhMjA3ODU5ZQ==
|
7
7
|
!binary "U0hBNTEy":
|
8
8
|
metadata.gz: !binary |-
|
9
|
-
|
10
|
-
|
11
|
-
|
9
|
+
YTFiNzcwMzg0NmQ2OGI5MmI2MTA1Njg1YzE2NjIzMDJiNjg5NDk4OGZmMDUw
|
10
|
+
YzlmMGI3NzJlMWNmNGViYTcwN2MxODY4MzA4OGRhNzZjOTJlOTg1ODUzZTcy
|
11
|
+
NzYyZjgzZjIzZmE2ZjFjMjEzZTIxZDE2OGJlNDEzMDE0MDNkOTU=
|
12
12
|
data.tar.gz: !binary |-
|
13
|
-
|
14
|
-
|
15
|
-
|
13
|
+
MjIxM2ViMmYzY2UyNDhhMWViNzI0NTgxODNmODRmZGEyODY4MGY0YWM0Mjcw
|
14
|
+
NWVmZjg1NDhjMjZjNDZjNzI2NTRmNjY3YzBlZjQwYjM4MmZkZmE2NzUwZjYx
|
15
|
+
OWMwMTRkOGJiMzBkNDA4OTY1NmU2NmY3NmQ1M2Q3YzdmOWE2NDc=
|
data/README.md
CHANGED
@@ -14,9 +14,17 @@ The project is hosted on the following sites:
|
|
14
14
|
|
15
15
|
## Introduction
|
16
16
|
|
17
|
-
[Ruby](http://www.ruby-lang.org/en/) is my favorite programming by far. As much as I love
|
18
|
-
|
19
|
-
|
17
|
+
[Ruby](http://www.ruby-lang.org/en/) is my favorite programming by far. As much as I love
|
18
|
+
Ruby I've always been a little disappointed that Ruby doesn't support function overloading.
|
19
|
+
Function overloading tends to reduce branching and keep function signatures simpler.
|
20
|
+
No sweat, I learned to do without. Then I started programming in [Erlang](http://www.erlang.org/)...
|
21
|
+
|
22
|
+
I've really started to enjoy working in Erlang. Erlang is good at all the things Ruby is bad
|
23
|
+
at and vice versa. Together, Ruby and Erlang make me happy. My favorite Erlang feature is,
|
24
|
+
without question, [pattern matching](http://learnyousomeerlang.com/syntax-in-functions#pattern-matching).
|
25
|
+
Pattern matching is like function overloading cranked to 11. So one day I was musing on Twitter
|
26
|
+
that I'd like to see Erlang-stype pattern matching in Ruby and one of my friends responded "Build it!"
|
27
|
+
So I did. And here it is.
|
20
28
|
|
21
29
|
### Goals
|
22
30
|
|
@@ -30,16 +38,21 @@ I've really started to enjoy working in Erlang. Erlang is good at all the things
|
|
30
38
|
|
31
39
|
### Features
|
32
40
|
|
33
|
-
*
|
41
|
+
* Pattern matching for instance methods.
|
42
|
+
* Pattern matching for object constructors.
|
34
43
|
* Parameter count matching
|
35
|
-
*
|
44
|
+
* Matching against primitive values
|
36
45
|
* Matching by class/datatype
|
37
46
|
* Matching against specific key/vaue pairs in hashes
|
38
47
|
* Matching against the presence of keys within hashes
|
39
|
-
*
|
40
|
-
*
|
48
|
+
* Implicit hash for last parameter
|
49
|
+
* Variable-length parameter lists
|
50
|
+
* Guard clauses
|
41
51
|
* Recursive calls to other pattern matches
|
52
|
+
* Recursive calls to superclass pattern matches
|
42
53
|
* Recursive calls to superclass methods
|
54
|
+
* Dispatching to superclass methods when no match is found
|
55
|
+
* Reasonable error messages when no match is found
|
43
56
|
|
44
57
|
### To-do
|
45
58
|
|
@@ -373,6 +386,19 @@ class Foo
|
|
373
386
|
end
|
374
387
|
```
|
375
388
|
|
389
|
+
### Constructor Overloading
|
390
|
+
|
391
|
+
```ruby
|
392
|
+
require 'pattern_matching'
|
393
|
+
|
394
|
+
class Foo
|
395
|
+
include PatternMatching
|
396
|
+
|
397
|
+
defn(:initialize) { @name = 'baz' }
|
398
|
+
defn(:initialize, _) {|name| @name = name.to_s }
|
399
|
+
end
|
400
|
+
```
|
401
|
+
|
376
402
|
### Matching by Class/Datatype
|
377
403
|
|
378
404
|
```ruby
|
data/lib/pattern_matching.rb
CHANGED
@@ -1,79 +1,93 @@
|
|
1
|
+
require 'pp'
|
2
|
+
|
1
3
|
module PatternMatching
|
2
4
|
|
3
|
-
VERSION = '0.
|
5
|
+
VERSION = '0.3.0'
|
4
6
|
|
5
7
|
UNBOUND = Class.new
|
6
8
|
ALL = Class.new
|
7
9
|
|
8
|
-
|
10
|
+
private
|
9
11
|
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
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
|
28
39
|
end
|
29
|
-
|
40
|
+
next
|
30
41
|
end
|
31
|
-
return
|
42
|
+
return false unless p == UNBOUND || p == arg
|
32
43
|
end
|
44
|
+
return true
|
45
|
+
end
|
33
46
|
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
end
|
43
|
-
elsif p.is_a?(Hash) || p == UNBOUND || p.is_a?(Class)
|
44
|
-
argv << args[i]
|
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
|
45
55
|
end
|
56
|
+
elsif p.is_a?(Hash) || p == UNBOUND || p.is_a?(Class)
|
57
|
+
argv << args[i]
|
46
58
|
end
|
47
|
-
return argv
|
48
59
|
end
|
60
|
+
return argv
|
61
|
+
end
|
49
62
|
|
50
|
-
|
51
|
-
|
52
|
-
args = args.first
|
63
|
+
def self.__pattern_match__(clazz, func, *args, &block) # :nodoc:
|
64
|
+
args = args.first
|
53
65
|
|
54
|
-
|
55
|
-
|
66
|
+
# get the array of matchers for this function
|
67
|
+
matchers = clazz.__function_pattern_matches__[func]
|
68
|
+
return [:nodef, nil] if matchers.nil?
|
56
69
|
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
end
|
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)
|
65
77
|
end
|
66
78
|
end
|
79
|
+
end
|
67
80
|
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
match = matchers[index]
|
73
|
-
argv = __unbound_args__(match, args)
|
74
|
-
return [:ok, self.instance_exec(*argv, &match[1])]
|
75
|
-
end
|
81
|
+
if index.nil?
|
82
|
+
return [:nomatch, nil]
|
83
|
+
else
|
84
|
+
return [:ok, matchers[index]]
|
76
85
|
end
|
86
|
+
end
|
87
|
+
|
88
|
+
protected
|
89
|
+
|
90
|
+
def self.included(base)
|
77
91
|
|
78
92
|
class << base
|
79
93
|
|
@@ -85,49 +99,45 @@ module PatternMatching
|
|
85
99
|
|
86
100
|
def defn(func, *args, &block)
|
87
101
|
|
88
|
-
guard = Class.new do
|
89
|
-
def initialize(func, clazz, matcher)
|
90
|
-
@func = func
|
91
|
-
@clazz = clazz
|
92
|
-
@matcher = matcher
|
93
|
-
end
|
94
|
-
def when(&block)
|
95
|
-
unless block_given?
|
96
|
-
raise ArgumentError.new("block missing for `when` guard on function `#{@func}` of class #{@clazz}")
|
97
|
-
end
|
98
|
-
@matcher[@matcher.length-1] = block
|
99
|
-
return nil
|
100
|
-
end
|
101
|
-
end
|
102
|
-
|
103
102
|
block = Proc.new{} unless block_given?
|
104
103
|
pattern = __add_pattern_for__(func, *args, &block)
|
105
104
|
|
106
105
|
unless self.instance_methods(false).include?(func)
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
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
|
111
114
|
super(*args, &block)
|
112
|
-
|
113
|
-
|
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
|
114
121
|
end
|
115
122
|
end
|
116
123
|
end
|
117
124
|
|
118
|
-
return
|
125
|
+
return PatternMatching::Guard.new(func, self, pattern)
|
119
126
|
end
|
120
127
|
|
121
|
-
|
128
|
+
public
|
129
|
+
|
130
|
+
def __function_pattern_matches__ # :nodoc:
|
131
|
+
@__function_pattern_matches__ ||= Hash.new
|
132
|
+
end
|
122
133
|
|
123
134
|
def __add_pattern_for__(func, *args, &block) # :nodoc:
|
124
135
|
block = Proc.new{} unless block_given?
|
125
|
-
matchers = self.
|
136
|
+
matchers = self.__function_pattern_matches__
|
126
137
|
matchers[func] = [] unless matchers.has_key?(func)
|
127
138
|
matchers[func] << [args, block, nil]
|
128
139
|
return matchers[func].last
|
129
140
|
end
|
130
|
-
|
131
141
|
end
|
132
142
|
end
|
133
143
|
end
|
data/spec/integration_spec.rb
CHANGED
@@ -1,10 +1,7 @@
|
|
1
1
|
require 'spec_helper'
|
2
2
|
require 'ostruct'
|
3
3
|
|
4
|
-
describe 'integration' do
|
5
|
-
|
6
4
|
class Bar
|
7
|
-
|
8
5
|
def greet
|
9
6
|
return 'Hello, World!'
|
10
7
|
end
|
@@ -15,9 +12,8 @@ describe 'integration' do
|
|
15
12
|
|
16
13
|
attr_accessor :name
|
17
14
|
|
18
|
-
|
19
|
-
|
20
|
-
end
|
15
|
+
defn(:initialize) { @name = 'baz' }
|
16
|
+
defn(:initialize, _) {|name| @name = name.to_s }
|
21
17
|
|
22
18
|
defn(:greet, _) do |name|
|
23
19
|
"Hello, #{name}!"
|
@@ -97,27 +93,46 @@ describe 'integration' do
|
|
97
93
|
defn(:all, ALL) { | args|
|
98
94
|
args
|
99
95
|
}
|
100
|
-
|
96
|
+
|
101
97
|
defn(:old_enough, _){ true }.when{|x| x >= 16 }
|
102
98
|
defn(:old_enough, _){ false }
|
103
|
-
|
99
|
+
|
104
100
|
defn(:right_age, _) {
|
105
101
|
true
|
106
102
|
}.when{|x| x >= 16 && x <= 104 }
|
107
|
-
|
103
|
+
|
108
104
|
defn(:right_age, _) {
|
109
105
|
false
|
110
106
|
}
|
111
|
-
|
107
|
+
|
112
108
|
defn(:wrong_age, _) {
|
113
109
|
true
|
114
110
|
}.when{|x| x < 16 || x > 104 }
|
115
|
-
|
111
|
+
|
116
112
|
defn(:wrong_age, _) {
|
117
113
|
false
|
118
114
|
}
|
119
115
|
end
|
120
116
|
|
117
|
+
class Baz < Foo
|
118
|
+
def boom_boom_room
|
119
|
+
'zoom zoom zoom'
|
120
|
+
end
|
121
|
+
def who(first, last)
|
122
|
+
[first, last].join(' ')
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
class Fizzbuzz < Baz
|
127
|
+
include PatternMatching
|
128
|
+
defn(:who, Integer) { |count|
|
129
|
+
(1..count).each.reduce(:+)
|
130
|
+
}
|
131
|
+
defn(:who) { 0 }
|
132
|
+
end
|
133
|
+
|
134
|
+
describe 'integration' do
|
135
|
+
|
121
136
|
let(:name) { 'Pattern Matcher' }
|
122
137
|
subject { Foo.new(name) }
|
123
138
|
|
@@ -129,6 +144,9 @@ describe 'integration' do
|
|
129
144
|
specify { subject.greet(:female, 'Jeri').should eq 'Hello, Ms. Jeri!' }
|
130
145
|
specify { subject.greet(:unknown, 'Jerry').should eq 'Hello, Jerry!' }
|
131
146
|
specify { subject.greet(nil, 'Jerry').should eq 'Goodbye, Jerry!' }
|
147
|
+
specify {
|
148
|
+
lambda { Foo.new.greet(1,2,3,4,5,6,7) }.should raise_error(NoMethodError)
|
149
|
+
}
|
132
150
|
|
133
151
|
specify { subject.options(bar: :baz, one: 1, many: 2).should == {bar: :baz, one: 1, many: 2} }
|
134
152
|
|
@@ -167,4 +185,21 @@ describe 'integration' do
|
|
167
185
|
specify { subject.wrong_age(10).should be_true }
|
168
186
|
specify { subject.wrong_age(110).should be_true }
|
169
187
|
|
188
|
+
context 'inheritance' do
|
189
|
+
|
190
|
+
specify { Fizzbuzz.new.greet(:male, 'Jerry').should eq 'Hello, Mr. Jerry!' }
|
191
|
+
specify { Fizzbuzz.new.greet(:female, 'Jeri').should eq 'Hello, Ms. Jeri!' }
|
192
|
+
specify { Fizzbuzz.new.greet(:unknown, 'Jerry').should eq 'Hello, Jerry!' }
|
193
|
+
specify { Fizzbuzz.new.greet(nil, 'Jerry').should eq 'Goodbye, Jerry!' }
|
194
|
+
|
195
|
+
specify { Fizzbuzz.new.who(5).should eq 15 }
|
196
|
+
specify { Fizzbuzz.new.who().should eq 0 }
|
197
|
+
specify {
|
198
|
+
lambda {
|
199
|
+
Fizzbuzz.new.who('Jerry', 'secret middle name', "D'Antonio")
|
200
|
+
}.should raise_error(NoMethodError)
|
201
|
+
}
|
202
|
+
|
203
|
+
specify { Fizzbuzz.new.boom_boom_room.should eq 'zoom zoom zoom' }
|
204
|
+
end
|
170
205
|
end
|
@@ -32,6 +32,21 @@ describe PatternMatching do
|
|
32
32
|
end
|
33
33
|
end
|
34
34
|
|
35
|
+
context 'constructor' do
|
36
|
+
|
37
|
+
it 'can pattern match the constructor' do
|
38
|
+
|
39
|
+
subject.defn(:initialize, PatternMatching::UNBOUND, PatternMatching::UNBOUND, PatternMatching::UNBOUND) { 'three args' }
|
40
|
+
subject.defn(:initialize, PatternMatching::UNBOUND, PatternMatching::UNBOUND) { 'two args' }
|
41
|
+
subject.defn(:initialize, PatternMatching::UNBOUND) { 'one arg' }
|
42
|
+
|
43
|
+
lambda { subject.new(1) }.should_not raise_error
|
44
|
+
lambda { subject.new(1, 2) }.should_not raise_error
|
45
|
+
lambda { subject.new(1, 2, 3) }.should_not raise_error
|
46
|
+
lambda { subject.new(1, 2, 3, 4) }.should raise_error
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
35
50
|
context 'parameter count' do
|
36
51
|
|
37
52
|
it 'does not match a call with not enough arguments' do
|
@@ -64,7 +79,7 @@ describe PatternMatching do
|
|
64
79
|
end
|
65
80
|
end
|
66
81
|
|
67
|
-
class UnmatchedCallTesterSubclass
|
82
|
+
class UnmatchedCallTesterSubclass < UnmatchedCallTesterSuperclass
|
68
83
|
include PatternMatching
|
69
84
|
defn(:foo) { 'baz' }
|
70
85
|
end
|
@@ -89,7 +104,7 @@ describe PatternMatching do
|
|
89
104
|
end
|
90
105
|
end
|
91
106
|
|
92
|
-
class RecursiveCallTesterSubclass
|
107
|
+
class RecursiveCallTesterSubclass < RecursiveCallTesterSuperclass
|
93
108
|
include PatternMatching
|
94
109
|
defn(:foo, :bar) { foo(:baz) }
|
95
110
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: pattern-matching
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.3.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Jerry D'Antonio
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2013-05-
|
11
|
+
date: 2013-05-22 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -45,7 +45,7 @@ homepage: https://github.com/jdantonio/pattern_matching/
|
|
45
45
|
licenses:
|
46
46
|
- MIT
|
47
47
|
metadata: {}
|
48
|
-
post_install_message:
|
48
|
+
post_install_message: start() -> io:format("Hello, World!").
|
49
49
|
rdoc_options: []
|
50
50
|
require_paths:
|
51
51
|
- lib
|