qo 0.4.0 → 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/.gitignore +3 -0
- data/README.md +50 -44
- data/Rakefile +106 -2
- data/lib/qo.rb +3 -1
- data/lib/qo/exceptions.rb +0 -13
- data/lib/qo/helpers.rb +12 -5
- data/lib/qo/matchers/array_matcher.rb +15 -0
- data/lib/qo/matchers/base_matcher.rb +15 -12
- data/lib/qo/matchers/guard_block_matcher.rb +53 -13
- data/lib/qo/matchers/hash_matcher.rb +6 -0
- data/lib/qo/matchers/pattern_match.rb +84 -52
- data/lib/qo/public_api.rb +62 -69
- data/lib/qo/version.rb +1 -1
- data/performance_report.txt +37 -37
- data/qo.gemspec +1 -0
- metadata +17 -4
- data/lib/qo/matchers/pattern_match_block.rb +0 -115
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 02dea6ad061112d957032940212ab369e09b0ec1
|
4
|
+
data.tar.gz: '037591f1119e010e7faba94c51f1ed4678cafe45'
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b01330722400077045a769eaaccc565ad2132f0413624b510f370754bfda109dac6ffcf1ea6cd2ce6987182d19cceb773bd04e67f49c990eef84f7a8774d53ea
|
7
|
+
data.tar.gz: df76eed79e30353032930972ef916a7788500753b4e0fd684cf42be4c8f2f2dded8493c9e43ed52e71c2023291952f1b2b6c319dfca0de9b9b64b951ed6dc1d7
|
data/.gitignore
CHANGED
data/README.md
CHANGED
@@ -52,43 +52,51 @@ How about some pattern matching? There are two styles:
|
|
52
52
|
|
53
53
|
#### Pattern Match
|
54
54
|
|
55
|
-
|
55
|
+
##### Case Statements
|
56
|
+
|
57
|
+
Qo case statements work much like a Ruby case statement, except in that they leverage
|
58
|
+
the full power of Qo matchers behind the scenes.
|
56
59
|
|
57
60
|
```ruby
|
58
61
|
# How about some "right-hand assignment" pattern matching
|
59
|
-
name_longer_than_three
|
60
|
-
people_with_truncated_names = people.map(&Qo.match(
|
61
|
-
Qo.m(name_longer_than_three) { |person| Person.new(person.name[0..2], person.age) },
|
62
|
-
Qo.m(Any) # Identity function, catch-all
|
63
|
-
))
|
62
|
+
name_longer_than_three = -> person { person.name.size > 3 }
|
64
63
|
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
64
|
+
person_with_truncated_name = Qo.case(people.first) { |m|
|
65
|
+
m.when(name_longer_than_three) { |person|
|
66
|
+
Person.new(person.name[0..2], person.age)
|
67
|
+
}
|
68
|
+
|
69
|
+
m.else
|
70
|
+
}
|
70
71
|
```
|
71
72
|
|
72
|
-
|
73
|
+
It takes in a value directly, and returns the result, much like a case statement.
|
74
|
+
|
75
|
+
Note that if `else` receives no block, it will default to an identity function
|
76
|
+
(`{ |v| v }`). If no else is provided and there's no match, you'll get back a nil.
|
77
|
+
You can write this out if you wish.
|
73
78
|
|
74
|
-
|
79
|
+
##### Match Statements
|
80
|
+
|
81
|
+
Match statements are like case statements, except in that they don't directly take
|
82
|
+
a value to match against. They're waiting for a value to come in later from
|
83
|
+
something else.
|
75
84
|
|
76
85
|
```ruby
|
77
|
-
name_longer_than_three
|
86
|
+
name_longer_than_three = -> person { person.name.size > 3 }
|
87
|
+
|
78
88
|
people_with_truncated_names = people.map(&Qo.match { |m|
|
79
89
|
m.when(name_longer_than_three) { |person| Person.new(person.name[0..2], person.age) }
|
80
|
-
m.else
|
90
|
+
m.else
|
81
91
|
})
|
82
92
|
|
83
93
|
# And standalone like a case:
|
84
|
-
Qo.match
|
94
|
+
Qo.match { |m|
|
85
95
|
m.when(age: 10..19) { |person| "#{person.name} is a teen that's #{person.age} years old" }
|
86
96
|
m.else { |person| "#{person.name} is #{person.age} years old" }
|
87
|
-
}
|
97
|
+
}.call(people.first)
|
88
98
|
```
|
89
99
|
|
90
|
-
(More details coming on the difference and planned 1.0.0 APIs)
|
91
|
-
|
92
100
|
### Qo'isms
|
93
101
|
|
94
102
|
Qo supports three main types of queries: `and`, `or`, and `not`.
|
@@ -439,18 +447,18 @@ people_hashes.select(&Qo[age: :nil?])
|
|
439
447
|
This is where I start going a bit off into the weeds. We're going to try and get RHA style pattern matching in Ruby.
|
440
448
|
|
441
449
|
```ruby
|
442
|
-
Qo.
|
443
|
-
|
444
|
-
|
445
|
-
|
450
|
+
Qo.case(['Robert', 22]) { |m|
|
451
|
+
m.when(Any, 20..99) { |n, a| "#{n} is an adult that is #{a} years old" }
|
452
|
+
m.else
|
453
|
+
}
|
446
454
|
# => "Robert is an adult that is 22 years old"
|
447
455
|
```
|
448
456
|
|
449
457
|
```ruby
|
450
|
-
Qo.
|
451
|
-
|
452
|
-
|
453
|
-
|
458
|
+
Qo.case(people_objects.first) { |m|
|
459
|
+
m.when(name: Any, age: 20..99) { |person| "#{person.name} is an adult that is #{person.age} years old" }
|
460
|
+
m.else
|
461
|
+
}
|
454
462
|
```
|
455
463
|
|
456
464
|
In this case it's trying to do a few things:
|
@@ -460,18 +468,14 @@ In this case it's trying to do a few things:
|
|
460
468
|
|
461
469
|
If no block function is provided, it assumes an identity function (`-> v { v }`) instead. If no match is found, `nil` will be returned.
|
462
470
|
|
463
|
-
If an initial target is not furnished, the matcher will become a curried proc awaiting a target. In more simple terms it just wants a target to run against, so let's give it a few with map:
|
464
|
-
|
465
471
|
```ruby
|
466
472
|
name_longer_than_three = -> person { person.name.size > 3 }
|
467
473
|
|
468
|
-
people_objects.map(&Qo.match
|
469
|
-
|
470
|
-
|
471
|
-
|
472
|
-
|
473
|
-
Qo.m(Any)
|
474
|
-
))
|
474
|
+
people_objects.map(&Qo.match { |m|
|
475
|
+
m.when(name_longer_than_three) { |person| Person.new(person.name[0..2], person.age) }
|
476
|
+
|
477
|
+
m.else
|
478
|
+
})
|
475
479
|
|
476
480
|
# => [Person(age: 22, name: "Rob"), Person(age: 22, name: "Rob"), Person(age: 42, name: "Foo"), Person(age: 17, name: "Bar")]
|
477
481
|
```
|
@@ -517,6 +521,8 @@ Qo.count_by([1,2,3,2,2,2,1], &:even?)
|
|
517
521
|
# }
|
518
522
|
```
|
519
523
|
|
524
|
+
This feature may be added to Ruby 2.6+: https://bugs.ruby-lang.org/issues/11076
|
525
|
+
|
520
526
|
### 5 - Hacky Fun Time
|
521
527
|
|
522
528
|
These examples will grow over the next few weeks as I think of more fun things to do with Qo. PRs welcome if you find fun uses!
|
@@ -573,10 +579,10 @@ HTTP responses.
|
|
573
579
|
|
574
580
|
```ruby
|
575
581
|
def get_url(url)
|
576
|
-
Net::HTTP.get_response(URI(url)).yield_self(&Qo.match
|
577
|
-
|
578
|
-
|
579
|
-
)
|
582
|
+
Net::HTTP.get_response(URI(url)).yield_self(&Qo.match { |m|
|
583
|
+
m.when(Net::HTTPSuccess) { |response| response.body.size },
|
584
|
+
m.else { |response| raise response.message }
|
585
|
+
})
|
580
586
|
end
|
581
587
|
|
582
588
|
get_url('https://github.com/baweaver/qo')
|
@@ -590,7 +596,7 @@ The difference between this and case? Well, the first is you can pass this to
|
|
590
596
|
be used in there, including predicate and content checks on the body:
|
591
597
|
|
592
598
|
```ruby
|
593
|
-
|
599
|
+
m.when(Net::HTTPSuccess, body: /Qo/)
|
594
600
|
```
|
595
601
|
|
596
602
|
You could put as many checks as you want in there, or use different Qo matchers
|
@@ -617,11 +623,11 @@ The nice thing about Unix style commands is that they use headers, which means C
|
|
617
623
|
```ruby
|
618
624
|
rows = CSV.new(`df -h`, col_sep: " ", headers: true).read.map(&:to_h)
|
619
625
|
|
620
|
-
rows.map(&Qo.match
|
621
|
-
|
626
|
+
rows.map(&Qo.match { |m|
|
627
|
+
m.when(Avail: /Gi$/) { |row|
|
622
628
|
"#{row['Filesystem']} mounted on #{row['Mounted']} [#{row['Avail']} / #{row['Size']}]"
|
623
629
|
}
|
624
|
-
)
|
630
|
+
}).compact
|
625
631
|
# => ["/dev/***** mounted on / [186Gi / 466Gi]"]
|
626
632
|
```
|
627
633
|
|
data/Rakefile
CHANGED
@@ -19,13 +19,13 @@ task :default => :spec
|
|
19
19
|
# "anything can be a key" bit of Ruby.
|
20
20
|
#
|
21
21
|
# @return [Unit] StdOut
|
22
|
-
def run_benchmark(title, **benchmarks)
|
22
|
+
def run_benchmark(title, quiet = false, **benchmarks)
|
23
23
|
puts '', title, '=' * title.size, ''
|
24
24
|
|
25
25
|
# Validation
|
26
26
|
benchmarks.each do |benchmark_name, benchmark_fn|
|
27
27
|
puts "#{benchmark_name} result: #{benchmark_fn.call()}"
|
28
|
-
end
|
28
|
+
end unless quiet
|
29
29
|
|
30
30
|
puts
|
31
31
|
|
@@ -147,6 +147,110 @@ task :perf do
|
|
147
147
|
)
|
148
148
|
end
|
149
149
|
|
150
|
+
task :perf_pattern_match do
|
151
|
+
# Going to redefine the way that success and fail happen in here.
|
152
|
+
return false
|
153
|
+
|
154
|
+
require 'dry-matcher'
|
155
|
+
|
156
|
+
# Match `[:ok, some_value]` for success
|
157
|
+
success_case = Dry::Matcher::Case.new(
|
158
|
+
match: -> value { value.first == :ok },
|
159
|
+
resolve: -> value { value.last }
|
160
|
+
)
|
161
|
+
|
162
|
+
# Match `[:err, some_error_code, some_value]` for failure
|
163
|
+
failure_case = Dry::Matcher::Case.new(
|
164
|
+
match: -> value, *pattern {
|
165
|
+
value[0] == :err && (pattern.any? ? pattern.include?(value[1]) : true)
|
166
|
+
},
|
167
|
+
resolve: -> value { value.last }
|
168
|
+
)
|
169
|
+
|
170
|
+
# Build the matcher
|
171
|
+
matcher = Dry::Matcher.new(success: success_case, failure: failure_case)
|
172
|
+
|
173
|
+
qo_m = Qo.match { |m|
|
174
|
+
m.success(Any) { |v| v }
|
175
|
+
m.failure(Any) { "ERR!" }
|
176
|
+
}
|
177
|
+
|
178
|
+
qo_m_case = proc { |target|
|
179
|
+
Qo.case(target) { |m|
|
180
|
+
m.success(Any) { |v| v }
|
181
|
+
m.failure(Any) { "ERR!" }
|
182
|
+
}
|
183
|
+
}
|
184
|
+
|
185
|
+
dm_m = proc { |target|
|
186
|
+
matcher.(target) do |m|
|
187
|
+
m.success { |(v)| v }
|
188
|
+
m.failure { 'ERR!' }
|
189
|
+
end
|
190
|
+
}
|
191
|
+
|
192
|
+
# v_m = proc { |target|
|
193
|
+
# next target[1] if target[0] == :ok
|
194
|
+
# 'ERR!'
|
195
|
+
# }
|
196
|
+
|
197
|
+
ok_target = [:ok, 12345]
|
198
|
+
err_target = [:err, "OH NO!"]
|
199
|
+
|
200
|
+
run_benchmark('Single Item Tuple',
|
201
|
+
'Qo': -> {
|
202
|
+
"OK: #{qo_m[ok_target]}, ERR: #{qo_m[err_target]}"
|
203
|
+
},
|
204
|
+
|
205
|
+
'Qo Case': -> {
|
206
|
+
"OK: #{qo_m_case[ok_target]}, ERR: #{qo_m_case[err_target]}"
|
207
|
+
},
|
208
|
+
|
209
|
+
'DryRB': -> {
|
210
|
+
"OK: #{dm_m[ok_target]}, ERR: #{dm_m[err_target]}"
|
211
|
+
},
|
212
|
+
|
213
|
+
# 'Vanilla': -> {
|
214
|
+
# "OK: #{v_m[ok_target]}, ERR: #{v_m[err_target]}"
|
215
|
+
# },
|
216
|
+
)
|
217
|
+
|
218
|
+
collection = [ok_target, err_target] * 2_000
|
219
|
+
|
220
|
+
run_benchmark('Large Tuple Collection', true,
|
221
|
+
'Qo': -> { collection.map(&qo_m) },
|
222
|
+
'Qo Case': -> { collection.map(&qo_m_case) },
|
223
|
+
'DryRB': -> { collection.map(&dm_m) },
|
224
|
+
# 'Vanilla': -> { collection.map(&v_m) }
|
225
|
+
)
|
226
|
+
|
227
|
+
# Person = Struct.new(:name, :age)
|
228
|
+
# people = [
|
229
|
+
# Person.new('Robert', 22),
|
230
|
+
# Person.new('Roberta', 22),
|
231
|
+
# Person.new('Foo', 42),
|
232
|
+
# Person.new('Bar', 17)
|
233
|
+
# ] * 1_000
|
234
|
+
|
235
|
+
# v_om = proc { |target|
|
236
|
+
# if /^F/.match?(target.name) && (30..50).include?(target.age)
|
237
|
+
# "It's foo!"
|
238
|
+
# else
|
239
|
+
# "Not foo"
|
240
|
+
# end
|
241
|
+
# }
|
242
|
+
|
243
|
+
# qo_om = Qo.match { |m|
|
244
|
+
# m.when(name: /^F/, age: 30..50) { "It's foo!" }
|
245
|
+
# m.else { "Not foo" }
|
246
|
+
# }
|
247
|
+
|
248
|
+
# run_benchmark('Large Object Collection', true,
|
249
|
+
# 'Qo': -> { people.map(&qo_om) },
|
250
|
+
# 'Vanilla': -> { people.map(&v_om) }
|
251
|
+
# )
|
252
|
+
end
|
253
|
+
|
150
254
|
# Below this mark are mostly my experiments to see what features perform a bit better
|
151
255
|
# than others, and are mostly left to check different versions of Ruby against eachother.
|
152
256
|
#
|
data/lib/qo.rb
CHANGED
@@ -11,7 +11,6 @@ require 'qo/matchers/guard_block_matcher'
|
|
11
11
|
|
12
12
|
# Meta Matchers
|
13
13
|
require 'qo/matchers/pattern_match'
|
14
|
-
require 'qo/matchers/pattern_match_block'
|
15
14
|
|
16
15
|
# Helpers
|
17
16
|
require 'qo/helpers'
|
@@ -21,6 +20,9 @@ require 'qo/exceptions'
|
|
21
20
|
require 'qo/public_api'
|
22
21
|
|
23
22
|
module Qo
|
23
|
+
# Identity function that returns its argument directly
|
24
|
+
IDENTITY = -> v { v }
|
25
|
+
|
24
26
|
extend Qo::Exceptions
|
25
27
|
extend Qo::Helpers
|
26
28
|
extend Qo::PublicApi
|
data/lib/qo/exceptions.rb
CHANGED
@@ -27,19 +27,6 @@ module Qo
|
|
27
27
|
end
|
28
28
|
end
|
29
29
|
|
30
|
-
# In the case of a Pattern Match, we need to ensure all arguments are
|
31
|
-
# GuardBlockMatchers.
|
32
|
-
#
|
33
|
-
# @author baweaver
|
34
|
-
# @since 0.2.0
|
35
|
-
#
|
36
|
-
class NotAllGuardMatchersProvided < ArgumentError
|
37
|
-
def to_s
|
38
|
-
"All provided matchers must be of type Qo::Matchers::GuardBlockMatcher " +
|
39
|
-
"defined with `Qo.matcher` or `Qo.m` instead of regular matchers."
|
40
|
-
end
|
41
|
-
end
|
42
|
-
|
43
30
|
# In the case of a Pattern Match, we should only have one "else" clause
|
44
31
|
#
|
45
32
|
# @author baweaver
|
data/lib/qo/helpers.rb
CHANGED
@@ -5,8 +5,11 @@ module Qo
|
|
5
5
|
# @note This method will attempt to coerce path segments to Symbols
|
6
6
|
# if unsuccessful in first dig.
|
7
7
|
#
|
8
|
-
# @param path_map
|
9
|
-
#
|
8
|
+
# @param path_map [String]
|
9
|
+
# Dot-delimited path
|
10
|
+
#
|
11
|
+
# @param expected_value [Any]
|
12
|
+
# Matcher
|
10
13
|
#
|
11
14
|
# @return [Proc]
|
12
15
|
# Hash -> Bool # Status of digging against the hash
|
@@ -22,10 +25,14 @@ module Qo
|
|
22
25
|
# Counts by a function. This is entirely because I hackney this everywhere in
|
23
26
|
# pry anyways, so I want a function to do it for me already.
|
24
27
|
#
|
25
|
-
# @param targets [Array[Any]]
|
26
|
-
#
|
28
|
+
# @param targets [Array[Any]]
|
29
|
+
# Targets to count
|
30
|
+
#
|
31
|
+
# @param &fn [Proc]
|
32
|
+
# Function to define count key
|
27
33
|
#
|
28
|
-
# @return [Hash[Any, Integer]]
|
34
|
+
# @return [Hash[Any, Integer]]
|
35
|
+
# Counts
|
29
36
|
def count_by(targets, &fn)
|
30
37
|
fn ||= -> v { v }
|
31
38
|
|
@@ -14,6 +14,14 @@ module Qo
|
|
14
14
|
# # => true
|
15
15
|
# ```
|
16
16
|
#
|
17
|
+
# It should be noted that arrays of dissimilar size will result in an instant
|
18
|
+
# false return value. If you wish to do a single value match, simply use the
|
19
|
+
# provided `Any` type as such:
|
20
|
+
#
|
21
|
+
# ```ruby
|
22
|
+
# array.select(&Any)
|
23
|
+
# ```
|
24
|
+
#
|
17
25
|
# In the case of an Array matching against an Object, it will match each provided
|
18
26
|
# matcher against the object.
|
19
27
|
#
|
@@ -33,6 +41,11 @@ module Qo
|
|
33
41
|
# @since 0.2.0
|
34
42
|
#
|
35
43
|
class ArrayMatcher < BaseMatcher
|
44
|
+
def initialize(type, array_matchers)
|
45
|
+
@array_matchers = array_matchers
|
46
|
+
@type = type
|
47
|
+
end
|
48
|
+
|
36
49
|
# Wrapper around call to allow for invocation in an Enumerable function,
|
37
50
|
# such as:
|
38
51
|
#
|
@@ -59,6 +72,8 @@ module Qo
|
|
59
72
|
return true if @array_matchers == target
|
60
73
|
|
61
74
|
if target.is_a?(::Array)
|
75
|
+
return false unless target.size == @array_matchers.size
|
76
|
+
|
62
77
|
match_with(@array_matchers.each_with_index) { |matcher, i|
|
63
78
|
match_value?(target[i], matcher)
|
64
79
|
}
|
@@ -22,10 +22,12 @@ module Qo
|
|
22
22
|
# @since 0.2.0
|
23
23
|
#
|
24
24
|
class BaseMatcher
|
25
|
-
def initialize(type,
|
26
|
-
@
|
27
|
-
|
28
|
-
@
|
25
|
+
def initialize(type, array_matchers, keyword_matchers)
|
26
|
+
@type = type
|
27
|
+
|
28
|
+
@full_matcher = array_matchers.empty? ?
|
29
|
+
Qo::Matchers::HashMatcher.new(type, keyword_matchers) :
|
30
|
+
Qo::Matchers::ArrayMatcher.new(type, array_matchers)
|
29
31
|
end
|
30
32
|
|
31
33
|
# Converts a Matcher to a proc for use in querying, such as:
|
@@ -34,9 +36,7 @@ module Qo
|
|
34
36
|
#
|
35
37
|
# @return [Proc[Any]]
|
36
38
|
def to_proc
|
37
|
-
@
|
38
|
-
Qo::Matchers::HashMatcher.new(@type, **@keyword_matchers).to_proc :
|
39
|
-
Qo::Matchers::ArrayMatcher.new(@type, *@array_matchers).to_proc
|
39
|
+
@full_matcher.to_proc
|
40
40
|
end
|
41
41
|
|
42
42
|
# You can directly call a matcher as well, much like a Proc,
|
@@ -46,11 +46,12 @@ module Qo
|
|
46
46
|
#
|
47
47
|
# @return [Boolean] Result of the match
|
48
48
|
def call(target)
|
49
|
-
|
49
|
+
@full_matcher.call(target)
|
50
50
|
end
|
51
51
|
|
52
52
|
alias_method :===, :call
|
53
53
|
alias_method :[], :call
|
54
|
+
alias_method :match?, :call
|
54
55
|
|
55
56
|
# Runs the relevant match method against the given collection with the
|
56
57
|
# given matcher function.
|
@@ -60,10 +61,12 @@ module Qo
|
|
60
61
|
#
|
61
62
|
# @return [Boolean] Result of the match
|
62
63
|
private def match_with(collection, &fn)
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
collection.
|
64
|
+
case @type
|
65
|
+
when 'and' then collection.all?(&fn)
|
66
|
+
when 'or' then collection.any?(&fn)
|
67
|
+
when 'not' then collection.none?(&fn)
|
68
|
+
else false
|
69
|
+
end
|
67
70
|
end
|
68
71
|
|
69
72
|
# Wraps a case equality statement to make it a bit easier to read. The
|
@@ -10,9 +10,9 @@ module Qo
|
|
10
10
|
# you're going to want to read that documentation first.
|
11
11
|
#
|
12
12
|
# @example
|
13
|
-
# Qo::Matchers::GuardBlockMatcher
|
13
|
+
# Qo::Matchers::GuardBlockMatcher
|
14
14
|
#
|
15
|
-
# guard_matcher = Qo.
|
15
|
+
# guard_matcher = Qo::Matchers::GuardBlockMatcher.new(Integer) { |v| v * 2 }
|
16
16
|
# guard_matcher.call(1) # => [true, 2]
|
17
17
|
# guard_matcher.call(:x) # => [false, false]
|
18
18
|
#
|
@@ -23,28 +23,68 @@ module Qo
|
|
23
23
|
# @since 0.1.5
|
24
24
|
#
|
25
25
|
class GuardBlockMatcher < BaseMatcher
|
26
|
-
# Identity function that returns its argument directly
|
27
|
-
IDENTITY = -> v { v }
|
28
|
-
|
29
26
|
# Definition of a non-match
|
30
27
|
NON_MATCH = [false, false]
|
31
28
|
|
32
|
-
def initialize(
|
33
|
-
@fn = fn || IDENTITY
|
29
|
+
def initialize(array_matchers, keyword_matchers, &fn)
|
30
|
+
@fn = fn || Qo::IDENTITY
|
31
|
+
|
32
|
+
super('and', array_matchers, keyword_matchers)
|
33
|
+
end
|
34
|
+
|
35
|
+
# Direct test of whether or not a target matches the GuardBlock's
|
36
|
+
# condition
|
37
|
+
#
|
38
|
+
# @param target [Any]
|
39
|
+
# Target value to match against
|
40
|
+
#
|
41
|
+
# @return [Boolean]
|
42
|
+
# Whether or not the target matched
|
43
|
+
def match?(target)
|
44
|
+
super(target)
|
45
|
+
end
|
34
46
|
|
35
|
-
|
47
|
+
# Forces a resolution of a match. Note that this method should
|
48
|
+
# not be used outside of pattern matches, as only a pattern
|
49
|
+
# match will have the necessary additional context to call
|
50
|
+
# it correctly.
|
51
|
+
#
|
52
|
+
# @param target [Any]
|
53
|
+
# Target value to match against
|
54
|
+
#
|
55
|
+
# @return [Any]
|
56
|
+
# Result of the function being called on the target
|
57
|
+
def match(target)
|
58
|
+
@fn.call(target)
|
36
59
|
end
|
37
60
|
|
38
61
|
# Overrides the base matcher's #to_proc to wrap the value in a status
|
39
62
|
# and potentially call through to the associated block if a base
|
40
63
|
# matcher would have passed
|
41
64
|
#
|
42
|
-
# @
|
43
|
-
#
|
65
|
+
# @see Qo::Matchers::GuardBlockMatcher#call
|
66
|
+
#
|
67
|
+
# @return [Proc[Any]]
|
68
|
+
# Function awaiting target value
|
44
69
|
def to_proc
|
45
|
-
Proc.new { |target|
|
46
|
-
|
47
|
-
|
70
|
+
Proc.new { |target| self.call(target) }
|
71
|
+
end
|
72
|
+
|
73
|
+
|
74
|
+
# Overrides the call method to wrap values in a return tuple to represent
|
75
|
+
# a positive match to guard against valid falsy returns
|
76
|
+
#
|
77
|
+
# @param target [Any]
|
78
|
+
# Target value to match against
|
79
|
+
#
|
80
|
+
# @return [Array[false, false]]
|
81
|
+
# The guard block did not match
|
82
|
+
#
|
83
|
+
# @return [Array[true, Any]]
|
84
|
+
# The guard block matched, and the provided function called through
|
85
|
+
# providing a return value.
|
86
|
+
def call(target)
|
87
|
+
super(target) ? [true, @fn.call(target)] : NON_MATCH
|
48
88
|
end
|
49
89
|
end
|
50
90
|
end
|
@@ -26,6 +26,11 @@ module Qo
|
|
26
26
|
# @since 0.2.0
|
27
27
|
#
|
28
28
|
class HashMatcher < BaseMatcher
|
29
|
+
def initialize(type, keyword_matchers)
|
30
|
+
@keyword_matchers = keyword_matchers
|
31
|
+
@type = type
|
32
|
+
end
|
33
|
+
|
29
34
|
# Wrapper around call to allow for invocation in an Enumerable function,
|
30
35
|
# such as:
|
31
36
|
#
|
@@ -95,6 +100,7 @@ module Qo
|
|
95
100
|
# @return [Boolean]
|
96
101
|
private def hash_case_match?(target, match_key, matcher)
|
97
102
|
return true if case_match?(target[match_key], matcher)
|
103
|
+
return false unless target.keys.first.is_a?(String)
|
98
104
|
|
99
105
|
match_key.respond_to?(:to_s) &&
|
100
106
|
target.key?(match_key.to_s) &&
|
@@ -2,71 +2,94 @@ require 'qo/exceptions'
|
|
2
2
|
|
3
3
|
module Qo
|
4
4
|
module Matchers
|
5
|
-
# Creates a PatternMatch
|
5
|
+
# Creates a PatternMatch in a succinct block format:
|
6
6
|
#
|
7
|
-
#
|
8
|
-
#
|
9
|
-
#
|
10
|
-
#
|
11
|
-
# This is done as an effort to emulate Right Hand Assignment seen in other
|
12
|
-
# functionally oriented languages pattern matching systems. Most notably this
|
13
|
-
# is done in Scala: (https://docs.scala-lang.org/tour/pattern-matching.html)
|
14
|
-
#
|
15
|
-
# ```scala
|
16
|
-
# notification match {
|
17
|
-
# case Email(email, title, _) =>
|
18
|
-
# s"You got an email from $email with title: $title"
|
19
|
-
#
|
20
|
-
# case SMS(number, message) =>
|
21
|
-
# s"You got an SMS from $number! Message: $message"
|
22
|
-
#
|
23
|
-
# case VoiceRecording(name, link) =>
|
24
|
-
# s"You received a Voice Recording from $name! Click the link to hear it: $link"
|
25
|
-
#
|
26
|
-
# case other => other
|
7
|
+
# ```ruby
|
8
|
+
# Qo.match(target) { |m|
|
9
|
+
# m.when(/^F/, 42) { |(name, age)| "#{name} is #{age}" }
|
10
|
+
# m.else { "We need a default, right?" }
|
27
11
|
# }
|
28
12
|
# ```
|
29
13
|
#
|
30
|
-
#
|
14
|
+
# The Public API obscures the fact that the matcher is only called when it
|
15
|
+
# is explicitly given an argument to match against. If it is not, it will
|
16
|
+
# just return a class waiting for a target, as such:
|
31
17
|
#
|
32
18
|
# ```ruby
|
33
|
-
#
|
34
|
-
#
|
35
|
-
#
|
36
|
-
#
|
37
|
-
#
|
38
|
-
#
|
39
|
-
#
|
40
|
-
#
|
41
|
-
#
|
42
|
-
#
|
43
|
-
#
|
44
|
-
# Qo.m(String, LINK_REGEX) { |name, link, _|
|
45
|
-
# "You received a Voice Recording from #{name}! Click the link to hear it: #{link}"
|
46
|
-
# },
|
47
|
-
#
|
48
|
-
# Qo.m(:*)
|
49
|
-
# )
|
19
|
+
# def get_url(url)
|
20
|
+
# Net::HTTP.get_response(URI(url)).yield_self(&Qo.match { |m|
|
21
|
+
# m.when(Net::HTTPSuccess) { |response| response.body.size }
|
22
|
+
# m.else { |response| raise response.message }
|
23
|
+
# })
|
24
|
+
# end
|
25
|
+
#
|
26
|
+
# get_url('https://github.com/baweaver/qo')
|
27
|
+
# # => 142387
|
28
|
+
# get_url('https://github.com/baweaver/qo/does_not_exist')
|
29
|
+
# # => RuntimeError: Not Found
|
50
30
|
# ```
|
51
31
|
#
|
52
|
-
#
|
53
|
-
#
|
32
|
+
# This is intended for flexibility between singular calls and calls as a
|
33
|
+
# paramater to higher order functions like `map` and `yield_self`.
|
54
34
|
#
|
55
|
-
#
|
35
|
+
# This variant was inspired by ideas from Scala, Haskell, and various Ruby
|
36
|
+
# libraries dealing with Async and self-yielding blocks. Especially notable
|
37
|
+
# were websocket handlers and dry-ruby implementations.
|
56
38
|
#
|
57
39
|
# @author baweaver
|
58
|
-
# @since 0.
|
40
|
+
# @since 0.3.0
|
59
41
|
#
|
60
42
|
class PatternMatch
|
61
|
-
def initialize
|
62
|
-
|
63
|
-
|
64
|
-
|
43
|
+
def initialize
|
44
|
+
@matchers = []
|
45
|
+
|
46
|
+
yield(self)
|
47
|
+
end
|
65
48
|
|
66
|
-
|
49
|
+
# Creates a match case. This is the exact same as any other `and` style
|
50
|
+
# match reflected in the public API, except that it's a Guard Block
|
51
|
+
# match being performed. That means if the left side matches, the right
|
52
|
+
# side function is invoked and that value is returned.
|
53
|
+
#
|
54
|
+
# @param *array_matchers [Array[Any]]
|
55
|
+
# Array style matchers
|
56
|
+
#
|
57
|
+
# @param **keyword_matchers [Hash[Any, Any]]
|
58
|
+
# Hash style matchers
|
59
|
+
#
|
60
|
+
# @param &fn [Proc]
|
61
|
+
# If matched, this function will be called. If no function is provided
|
62
|
+
# Qo will default to identity
|
63
|
+
#
|
64
|
+
# @return [Array[GuardBlockMatcher]]
|
65
|
+
# The return of this method should not be directly depended on, but will
|
66
|
+
# provide all matchers currently present. This will likely be left for
|
67
|
+
# ease of debugging later.
|
68
|
+
def when(*array_matchers, **keyword_matchers, &fn)
|
69
|
+
@matchers << Qo::Matchers::GuardBlockMatcher.new(
|
70
|
+
array_matchers,
|
71
|
+
keyword_matchers,
|
72
|
+
&(fn || Qo::IDENTITY)
|
73
|
+
)
|
67
74
|
end
|
68
75
|
|
69
|
-
#
|
76
|
+
# Else is the last statement that will be evaluated if all other parts
|
77
|
+
# fail. It should be noted that it won't magically appear, you have to
|
78
|
+
# explicitly put an `else` case in for it to catch on no match unless
|
79
|
+
# you want a `nil` return
|
80
|
+
#
|
81
|
+
# @param &fn [Proc]
|
82
|
+
# Function to call when all other matches have failed. If no value is
|
83
|
+
# provided, it assumes `Qo::IDENTITY` which will return the value given.
|
84
|
+
#
|
85
|
+
# @return [Proc]
|
86
|
+
def else(&fn)
|
87
|
+
raise Qo::Exceptions::MultipleElseClauses if @else
|
88
|
+
|
89
|
+
@else = fn || Qo::IDENTITY
|
90
|
+
end
|
91
|
+
|
92
|
+
# Proc version of a PatternMatch
|
70
93
|
#
|
71
94
|
# @return [Proc]
|
72
95
|
# Any -> Any | nil
|
@@ -80,15 +103,24 @@ module Qo
|
|
80
103
|
# Target to run against and pipe to the associated block if it
|
81
104
|
# "matches" any of the GuardBlocks
|
82
105
|
#
|
83
|
-
# @return [Any
|
106
|
+
# @return [Any]
|
107
|
+
# Result of the piped block
|
108
|
+
#
|
109
|
+
# @return [nil]
|
110
|
+
# No matches were found, so nothing is returned
|
84
111
|
def call(target)
|
85
112
|
@matchers.each { |guard_block_matcher|
|
86
|
-
|
87
|
-
return
|
113
|
+
next unless guard_block_matcher.match?(target)
|
114
|
+
return guard_block_matcher.match(target)
|
88
115
|
}
|
89
116
|
|
117
|
+
return @else.call(target) if @else
|
118
|
+
|
90
119
|
nil
|
91
120
|
end
|
121
|
+
|
122
|
+
alias_method :===, :call
|
123
|
+
alias_method :[], :call
|
92
124
|
end
|
93
125
|
end
|
94
126
|
end
|
data/lib/qo/public_api.rb
CHANGED
@@ -15,8 +15,11 @@ module Qo
|
|
15
15
|
# must pass to be considered a "match". It will short-circuit in the case of
|
16
16
|
# a false match.
|
17
17
|
#
|
18
|
-
# @param *array_matchers
|
19
|
-
#
|
18
|
+
# @param *array_matchers [Array]
|
19
|
+
# Array-like conditionals
|
20
|
+
#
|
21
|
+
# @param **keyword_matchers [Hash]
|
22
|
+
# Keyword style conditionals
|
20
23
|
#
|
21
24
|
# @return [Proc[Any]]
|
22
25
|
# Any -> Bool # Given a target, will return if it "matches"
|
@@ -31,8 +34,11 @@ module Qo
|
|
31
34
|
# must pass to be considered a "match". It will short-circuit in the case of
|
32
35
|
# a true match.
|
33
36
|
#
|
34
|
-
# @param *array_matchers
|
35
|
-
#
|
37
|
+
# @param *array_matchers [Array]
|
38
|
+
# Array-like conditionals
|
39
|
+
#
|
40
|
+
# @param **keyword_matchers [Hash]
|
41
|
+
# Keyword style conditionals
|
36
42
|
#
|
37
43
|
# @return [Proc[Any]]
|
38
44
|
# Any -> Bool # Given a target, will return if it "matches"
|
@@ -44,8 +50,11 @@ module Qo
|
|
44
50
|
# should pass to be considered a "match". It will short-circuit in the case of
|
45
51
|
# a true match.
|
46
52
|
#
|
47
|
-
# @param *array_matchers
|
48
|
-
#
|
53
|
+
# @param *array_matchers [Array]
|
54
|
+
# Array-like conditionals
|
55
|
+
#
|
56
|
+
# @param **keyword_matchers [Hash]
|
57
|
+
# Keyword style conditionals
|
49
58
|
#
|
50
59
|
# @return [Proc[Any]]
|
51
60
|
# Any -> Bool # Given a target, will return if it "matches"
|
@@ -53,90 +62,74 @@ module Qo
|
|
53
62
|
create_matcher('not', array_matchers, keyword_matchers)
|
54
63
|
end
|
55
64
|
|
56
|
-
#
|
57
|
-
#
|
58
|
-
#
|
59
|
-
# the left-hand matcher passes. Once called with a value, it will either
|
60
|
-
# return `[false, false]` or `[true, Any]`.
|
65
|
+
# A pattern match will try and run all guard block style matchers in sequence
|
66
|
+
# until it finds one that "matches". Once found, it will pass the target
|
67
|
+
# into the associated matcher's block function.
|
61
68
|
#
|
62
|
-
#
|
63
|
-
#
|
69
|
+
# @example
|
70
|
+
# [1,2,3].map(&Qo.match { |m|
|
71
|
+
# m.when(:even?) { |v| v * 3 }
|
72
|
+
# m.else { |v| v - 1 }
|
73
|
+
# })
|
74
|
+
# => [0, 6, 2]
|
64
75
|
#
|
65
|
-
# @param
|
66
|
-
#
|
67
|
-
# @param &fn [Proc] Guarded function
|
76
|
+
# @param fn [Proc]
|
77
|
+
# Body of the matcher, as shown in examples
|
68
78
|
#
|
69
|
-
# @return [
|
70
|
-
#
|
71
|
-
def
|
72
|
-
|
73
|
-
end
|
79
|
+
# @return [Qo::PatternMatch]
|
80
|
+
# A function awaiting a value to match against
|
81
|
+
def match(&fn)
|
82
|
+
return proc { false } unless block_given?
|
74
83
|
|
75
|
-
|
76
|
-
|
84
|
+
Qo::Matchers::PatternMatch.new(&fn)
|
85
|
+
end
|
77
86
|
|
78
|
-
#
|
79
|
-
#
|
87
|
+
# Similar to the common case statement of Ruby, except in that it behaves
|
88
|
+
# as if `Array#===` and `Hash#===` exist in the form of Qo matchers.
|
80
89
|
#
|
81
|
-
#
|
82
|
-
#
|
83
|
-
# associated matcher's block function.
|
90
|
+
# @note
|
91
|
+
# I refer to the potential 2.6+ features currently being discussed here:
|
84
92
|
#
|
85
|
-
#
|
86
|
-
#
|
87
|
-
# PatternMatchBlock instead. If any args are provided, the first
|
88
|
-
# will be treated as the target.
|
93
|
+
# * `Hash#===` - https://bugs.ruby-lang.org/issues/14869
|
94
|
+
# * `Array#===` - https://bugs.ruby-lang.org/issues/14916
|
89
95
|
#
|
90
|
-
# @
|
91
|
-
# Collection of matchers to run, potentially prefixed by a target object
|
96
|
+
# @see Qo#match
|
92
97
|
#
|
93
|
-
# @
|
94
|
-
#
|
95
|
-
#
|
98
|
+
# @example
|
99
|
+
# Qo.case([1, 1]) { |m|
|
100
|
+
# m.when(Any, Any) { |a, b| a + b }
|
101
|
+
# m.else { |v| v }
|
102
|
+
# }
|
103
|
+
# => 2
|
96
104
|
#
|
97
|
-
# @
|
98
|
-
#
|
99
|
-
#
|
105
|
+
# @param value [Any]
|
106
|
+
# Value to match against
|
107
|
+
#
|
108
|
+
# @param &fn [Proc]
|
109
|
+
# Body of the matcher, as shown above
|
100
110
|
#
|
101
111
|
# @return [Any]
|
102
|
-
#
|
103
|
-
|
104
|
-
|
105
|
-
if block_given?
|
106
|
-
return args.empty? ?
|
107
|
-
Qo::Matchers::PatternMatchBlock.new(&fn) :
|
108
|
-
Qo::Matchers::PatternMatchBlock.new(&fn).call(args.first)
|
109
|
-
end
|
110
|
-
|
111
|
-
if args.first.is_a?(Qo::Matchers::GuardBlockMatcher)
|
112
|
-
Qo::Matchers::PatternMatch.new(*args)
|
113
|
-
else
|
114
|
-
match_target, *qo_matchers = args
|
115
|
-
Qo::Matchers::PatternMatch.new(*qo_matchers).call(match_target)
|
116
|
-
end
|
112
|
+
# The result of calling a pattern match with a provided value
|
113
|
+
def case(value, &fn)
|
114
|
+
Qo::Matchers::PatternMatch.new(&fn).call(value)
|
117
115
|
end
|
118
116
|
|
119
117
|
# Abstraction for creating a matcher, allowing for common error handling scenarios.
|
120
118
|
#
|
121
|
-
# @param type [String]
|
122
|
-
#
|
123
|
-
#
|
119
|
+
# @param type [String]
|
120
|
+
# Type of matcher
|
121
|
+
#
|
122
|
+
# @param array_matchers [Array[Any]]
|
123
|
+
# Array-like conditionals
|
124
124
|
#
|
125
|
-
# @
|
126
|
-
#
|
125
|
+
# @param keyword_matchers [Hash[Any, Any]]
|
126
|
+
# Keyword style conditionals
|
127
127
|
#
|
128
128
|
# @return [Qo::Matcher]
|
129
129
|
private def create_matcher(type, array_matchers, keyword_matchers)
|
130
|
-
|
131
|
-
|
132
|
-
raise Qo::NoMatchersProvided if array_empty && hash_empty
|
133
|
-
raise Qo::MultipleMatchersProvided if !(array_empty || hash_empty)
|
130
|
+
raise MultipleMatchersProvided if !array_matchers.empty? && !keyword_matchers.empty?
|
134
131
|
|
135
|
-
|
136
|
-
Qo::Matchers::ArrayMatcher.new(type, *array_matchers)
|
137
|
-
else
|
138
|
-
Qo::Matchers::HashMatcher.new(type, **keyword_matchers)
|
139
|
-
end
|
132
|
+
Qo::Matchers::BaseMatcher.new(type, array_matchers, keyword_matchers)
|
140
133
|
end
|
141
134
|
end
|
142
135
|
end
|
data/lib/qo/version.rb
CHANGED
data/performance_report.txt
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
Running on Qo v0.
|
1
|
+
Running on Qo v0.5.0 at rev 7b49c20d29630d9f56328d4663bb1b1ce1add2b0
|
2
2
|
- Ruby ruby 2.5.1p57 (2018-03-29 revision 63029) [x86_64-darwin17]
|
3
3
|
|
4
4
|
Array * Array - Literal
|
@@ -8,15 +8,15 @@ Vanilla result: true
|
|
8
8
|
Qo.and result: true
|
9
9
|
|
10
10
|
Warming up --------------------------------------
|
11
|
-
Vanilla
|
12
|
-
Qo.and
|
11
|
+
Vanilla 245.363k i/100ms
|
12
|
+
Qo.and 67.365k i/100ms
|
13
13
|
Calculating -------------------------------------
|
14
|
-
Vanilla
|
15
|
-
Qo.and
|
14
|
+
Vanilla 7.823M (± 4.0%) i/s - 39.258M in 5.027372s
|
15
|
+
Qo.and 859.133k (± 2.3%) i/s - 4.311M in 5.020923s
|
16
16
|
|
17
17
|
Comparison:
|
18
|
-
Vanilla:
|
19
|
-
Qo.and:
|
18
|
+
Vanilla: 7822633.5 i/s
|
19
|
+
Qo.and: 859133.0 i/s - 9.11x slower
|
20
20
|
|
21
21
|
|
22
22
|
Array * Array - Index pattern match
|
@@ -26,15 +26,15 @@ Vanilla result: true
|
|
26
26
|
Qo.and result: true
|
27
27
|
|
28
28
|
Warming up --------------------------------------
|
29
|
-
Vanilla
|
30
|
-
Qo.and
|
29
|
+
Vanilla 43.805k i/100ms
|
30
|
+
Qo.and 21.434k i/100ms
|
31
31
|
Calculating -------------------------------------
|
32
|
-
Vanilla
|
33
|
-
Qo.and
|
32
|
+
Vanilla 511.690k (± 2.0%) i/s - 2.584M in 5.053034s
|
33
|
+
Qo.and 241.516k (± 2.7%) i/s - 1.222M in 5.062575s
|
34
34
|
|
35
35
|
Comparison:
|
36
|
-
Vanilla:
|
37
|
-
Qo.and:
|
36
|
+
Vanilla: 511689.7 i/s
|
37
|
+
Qo.and: 241515.5 i/s - 2.12x slower
|
38
38
|
|
39
39
|
|
40
40
|
Array * Object - Predicate match
|
@@ -44,15 +44,15 @@ Vanilla result: false
|
|
44
44
|
Qo.and result: false
|
45
45
|
|
46
46
|
Warming up --------------------------------------
|
47
|
-
Vanilla
|
48
|
-
Qo.and
|
47
|
+
Vanilla 129.649k i/100ms
|
48
|
+
Qo.and 25.903k i/100ms
|
49
49
|
Calculating -------------------------------------
|
50
|
-
Vanilla 2.
|
51
|
-
Qo.and
|
50
|
+
Vanilla 2.049M (± 2.7%) i/s - 10.242M in 5.002097s
|
51
|
+
Qo.and 287.416k (± 3.8%) i/s - 1.451M in 5.054898s
|
52
52
|
|
53
53
|
Comparison:
|
54
|
-
Vanilla:
|
55
|
-
Qo.and:
|
54
|
+
Vanilla: 2049180.0 i/s
|
55
|
+
Qo.and: 287416.4 i/s - 7.13x slower
|
56
56
|
|
57
57
|
|
58
58
|
Array * Array - Select index pattern match
|
@@ -62,15 +62,15 @@ Vanilla result: [["Robert", 22], ["Roberta", 22]]
|
|
62
62
|
Qo.and result: [["Robert", 22], ["Roberta", 22]]
|
63
63
|
|
64
64
|
Warming up --------------------------------------
|
65
|
-
Vanilla
|
66
|
-
Qo.and
|
65
|
+
Vanilla 12.729k i/100ms
|
66
|
+
Qo.and 6.911k i/100ms
|
67
67
|
Calculating -------------------------------------
|
68
|
-
Vanilla
|
69
|
-
Qo.and
|
68
|
+
Vanilla 135.430k (± 1.8%) i/s - 687.366k in 5.077139s
|
69
|
+
Qo.and 71.615k (± 2.8%) i/s - 359.372k in 5.022246s
|
70
70
|
|
71
71
|
Comparison:
|
72
|
-
Vanilla:
|
73
|
-
Qo.and:
|
72
|
+
Vanilla: 135430.5 i/s
|
73
|
+
Qo.and: 71615.2 i/s - 1.89x slower
|
74
74
|
|
75
75
|
|
76
76
|
Hash * Hash - Hash intersection
|
@@ -80,15 +80,15 @@ Vanilla result: [{:name=>"Robert", :age=>22}, {:name=>"Roberta", :age=>22}]
|
|
80
80
|
Qo.and result: [{:name=>"Robert", :age=>22}, {:name=>"Roberta", :age=>22}]
|
81
81
|
|
82
82
|
Warming up --------------------------------------
|
83
|
-
Vanilla
|
84
|
-
Qo.and 5.
|
83
|
+
Vanilla 33.461k i/100ms
|
84
|
+
Qo.and 5.366k i/100ms
|
85
85
|
Calculating -------------------------------------
|
86
|
-
Vanilla
|
87
|
-
Qo.and
|
86
|
+
Vanilla 366.234k (± 3.1%) i/s - 1.840M in 5.030236s
|
87
|
+
Qo.and 54.974k (± 4.4%) i/s - 279.032k in 5.087315s
|
88
88
|
|
89
89
|
Comparison:
|
90
|
-
Vanilla:
|
91
|
-
Qo.and:
|
90
|
+
Vanilla: 366233.9 i/s
|
91
|
+
Qo.and: 54973.5 i/s - 6.66x slower
|
92
92
|
|
93
93
|
|
94
94
|
Hash * Object - Property match
|
@@ -98,13 +98,13 @@ Vanilla result: [#<struct Person name="Robert", age=22>, #<struct Person name="R
|
|
98
98
|
Qo.and result: [#<struct Person name="Robert", age=22>, #<struct Person name="Roberta", age=22>]
|
99
99
|
|
100
100
|
Warming up --------------------------------------
|
101
|
-
Vanilla
|
102
|
-
Qo.and 5.
|
101
|
+
Vanilla 33.166k i/100ms
|
102
|
+
Qo.and 5.659k i/100ms
|
103
103
|
Calculating -------------------------------------
|
104
|
-
Vanilla
|
105
|
-
Qo.and
|
104
|
+
Vanilla 371.150k (± 3.4%) i/s - 1.857M in 5.010508s
|
105
|
+
Qo.and 58.451k (± 3.4%) i/s - 294.268k in 5.040637s
|
106
106
|
|
107
107
|
Comparison:
|
108
|
-
Vanilla:
|
109
|
-
Qo.and:
|
108
|
+
Vanilla: 371149.8 i/s
|
109
|
+
Qo.and: 58450.6 i/s - 6.35x slower
|
110
110
|
|
data/qo.gemspec
CHANGED
@@ -28,6 +28,7 @@ Gem::Specification.new do |spec|
|
|
28
28
|
spec.add_development_dependency "benchmark-ips"
|
29
29
|
spec.add_development_dependency "yard"
|
30
30
|
spec.add_development_dependency "redcarpet"
|
31
|
+
spec.add_development_dependency "dry-matcher"
|
31
32
|
|
32
33
|
spec.add_runtime_dependency "any", '0.1.0'
|
33
34
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: qo
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.5.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Brandon Weaver
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2018-08-
|
11
|
+
date: 2018-08-09 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -122,6 +122,20 @@ dependencies:
|
|
122
122
|
- - ">="
|
123
123
|
- !ruby/object:Gem::Version
|
124
124
|
version: '0'
|
125
|
+
- !ruby/object:Gem::Dependency
|
126
|
+
name: dry-matcher
|
127
|
+
requirement: !ruby/object:Gem::Requirement
|
128
|
+
requirements:
|
129
|
+
- - ">="
|
130
|
+
- !ruby/object:Gem::Version
|
131
|
+
version: '0'
|
132
|
+
type: :development
|
133
|
+
prerelease: false
|
134
|
+
version_requirements: !ruby/object:Gem::Requirement
|
135
|
+
requirements:
|
136
|
+
- - ">="
|
137
|
+
- !ruby/object:Gem::Version
|
138
|
+
version: '0'
|
125
139
|
- !ruby/object:Gem::Dependency
|
126
140
|
name: any
|
127
141
|
requirement: !ruby/object:Gem::Requirement
|
@@ -199,7 +213,6 @@ files:
|
|
199
213
|
- lib/qo/matchers/guard_block_matcher.rb
|
200
214
|
- lib/qo/matchers/hash_matcher.rb
|
201
215
|
- lib/qo/matchers/pattern_match.rb
|
202
|
-
- lib/qo/matchers/pattern_match_block.rb
|
203
216
|
- lib/qo/public_api.rb
|
204
217
|
- lib/qo/version.rb
|
205
218
|
- performance_report.txt
|
@@ -224,7 +237,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
224
237
|
version: '0'
|
225
238
|
requirements: []
|
226
239
|
rubyforge_project:
|
227
|
-
rubygems_version: 2.
|
240
|
+
rubygems_version: 2.6.14.1
|
228
241
|
signing_key:
|
229
242
|
specification_version: 4
|
230
243
|
summary: Qo is a querying library for Ruby pattern matching
|
@@ -1,115 +0,0 @@
|
|
1
|
-
require 'qo/exceptions'
|
2
|
-
|
3
|
-
module Qo
|
4
|
-
module Matchers
|
5
|
-
# Creates a PatternMatch in the style of a block.
|
6
|
-
#
|
7
|
-
# This varies from the regular PatternMatch in that all matchers are
|
8
|
-
# provided in a more succinct block format:
|
9
|
-
#
|
10
|
-
# ```ruby
|
11
|
-
# Qo.match(target) { |m|
|
12
|
-
# m.when(/^F/, 42) { |(name, age)| "#{name} is #{age}" }
|
13
|
-
# m.else { "We need a default, right?" }
|
14
|
-
# }
|
15
|
-
# ```
|
16
|
-
#
|
17
|
-
# The Public API obscures the fact that the matcher is only called when it
|
18
|
-
# is explicitly given an argument to match against. If it is not, it will
|
19
|
-
# just return a class waiting for a target, as such:
|
20
|
-
#
|
21
|
-
# ```ruby
|
22
|
-
# def get_url(url)
|
23
|
-
# Net::HTTP.get_response(URI(url)).yield_self(&Qo.match { |m|
|
24
|
-
# m.when(Net::HTTPSuccess) { |response| response.body.size }
|
25
|
-
# m.else { |response| raise response.message }
|
26
|
-
# })
|
27
|
-
# end
|
28
|
-
#
|
29
|
-
# get_url('https://github.com/baweaver/qo')
|
30
|
-
# # => 142387
|
31
|
-
# get_url('https://github.com/baweaver/qo/does_not_exist')
|
32
|
-
# # => RuntimeError: Not Found
|
33
|
-
# ```
|
34
|
-
#
|
35
|
-
# This is intended for flexibility between singular calls and calls as a
|
36
|
-
# paramater to higher order functions like `map` and `yield_self`.
|
37
|
-
#
|
38
|
-
# This variant was inspired by ideas from Scala, Haskell, and various Ruby
|
39
|
-
# libraries dealing with Async and self-yielding blocks. Especially notable
|
40
|
-
# were websocket handlers and dry-ruby implementations.
|
41
|
-
#
|
42
|
-
# @author baweaver
|
43
|
-
# @since 0.3.0
|
44
|
-
#
|
45
|
-
class PatternMatchBlock
|
46
|
-
def initialize
|
47
|
-
@matchers = []
|
48
|
-
|
49
|
-
yield(self)
|
50
|
-
end
|
51
|
-
|
52
|
-
# Creates a match case. This is the exact same as any other `and` style
|
53
|
-
# match reflected in the public API, except that it's a Guard Block
|
54
|
-
# match being performed. That means if the left side matches, the right
|
55
|
-
# side function is invoked and that value is returned.
|
56
|
-
#
|
57
|
-
# @param *array_matchers [Array[Any]]
|
58
|
-
# Array style matchers
|
59
|
-
#
|
60
|
-
# @param **keyword_matchers [Hash[Any, Any]]
|
61
|
-
# Hash style matchers
|
62
|
-
#
|
63
|
-
# @param &fn [Proc]
|
64
|
-
# If matched, this function will be called
|
65
|
-
#
|
66
|
-
# @return [Array[GuardBlockMatcher]]
|
67
|
-
# The return of this method should not be directly depended on, but will
|
68
|
-
# provide all matchers currently present. This will likely be left for
|
69
|
-
# ease of debugging later.
|
70
|
-
def when(*array_matchers, **keyword_matchers, &fn)
|
71
|
-
@matchers << Qo::Matchers::GuardBlockMatcher.new(*array_matchers, **keyword_matchers, &fn)
|
72
|
-
end
|
73
|
-
|
74
|
-
# Else is the last statement that will be evaluated if all other parts
|
75
|
-
# fail. It should be noted that it won't magically appear, you have to
|
76
|
-
# explicitly put an `else` case in for it to catch on no match unless
|
77
|
-
# you want a `nil` return
|
78
|
-
#
|
79
|
-
# @param &fn [Proc]
|
80
|
-
# Function to call when all other matches have failed
|
81
|
-
#
|
82
|
-
# @return [Proc]
|
83
|
-
def else(&fn)
|
84
|
-
raise Qo::Exceptions::MultipleElseClauses if @else
|
85
|
-
@else = fn
|
86
|
-
end
|
87
|
-
|
88
|
-
# Proc version of a PatternMatchBlock
|
89
|
-
#
|
90
|
-
# @return [Proc]
|
91
|
-
# Any -> Any | nil
|
92
|
-
def to_proc
|
93
|
-
Proc.new { |target| self.call(target) }
|
94
|
-
end
|
95
|
-
|
96
|
-
# Immediately invokes a PatternMatch
|
97
|
-
#
|
98
|
-
# @param target [Any]
|
99
|
-
# Target to run against and pipe to the associated block if it
|
100
|
-
# "matches" any of the GuardBlocks
|
101
|
-
#
|
102
|
-
# @return [Any | nil] Result of the piped block, or nil on a miss
|
103
|
-
def call(target)
|
104
|
-
@matchers.each { |guard_block_matcher|
|
105
|
-
did_match, match_result = guard_block_matcher.call(target)
|
106
|
-
return match_result if did_match
|
107
|
-
}
|
108
|
-
|
109
|
-
return @else.call(target) if @else
|
110
|
-
|
111
|
-
nil
|
112
|
-
end
|
113
|
-
end
|
114
|
-
end
|
115
|
-
end
|