pattern-matching 0.2.0 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|