qo 0.4.0 → 0.5.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 +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
|