qo 0.2.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +27 -1
- data/Rakefile +1 -1
- data/lib/qo.rb +1 -0
- data/lib/qo/exceptions.rb +11 -0
- data/lib/qo/matchers/pattern_match_block.rb +115 -0
- data/lib/qo/public_api.rb +23 -3
- data/lib/qo/version.rb +1 -1
- data/performance_report.txt +38 -37
- data/qo.gemspec +1 -1
- metadata +4 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: af6c00578bf0fca7464a3823282f8de93e3f5befde82c991e1f4cee97c244c1a
|
4
|
+
data.tar.gz: 9532c9394d8874306487846fe166c11315a5e322af62d58a40ee8f5a59257cf1
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 71953edde27da9876463b453c8b8909986ea8f18461fa5c9ae123a15c71fea739276dd768a85487fc51152e6e3d3e0f0c9eecbc09db95eb97fc7433584caa2b0
|
7
|
+
data.tar.gz: 754e7dce63ca7a0e9b641fe45eb2b06feeb3045804f9a3236088c0187781b50de5422eaf506f777cd92f24a674ad1ca8d5dbc2faa6eac4fdfe53cdde9b007314
|
data/README.md
CHANGED
@@ -43,7 +43,15 @@ end
|
|
43
43
|
|
44
44
|
# Run a select like an AR query, getting the age attribute against a range
|
45
45
|
people.select(&Qo[age: 18..30])
|
46
|
+
```
|
47
|
+
|
48
|
+
How about some pattern matching? There are two styles:
|
49
|
+
|
50
|
+
#### Pattern Match
|
51
|
+
|
52
|
+
The original style
|
46
53
|
|
54
|
+
```
|
47
55
|
# How about some "right-hand assignment" pattern matching
|
48
56
|
name_longer_than_three = -> person { person.name.size > 3 }
|
49
57
|
people_with_truncated_names = people.map(&Qo.match(
|
@@ -58,7 +66,25 @@ Qo.match(people.first,
|
|
58
66
|
)
|
59
67
|
```
|
60
68
|
|
61
|
-
|
69
|
+
#### Pattern Match Block
|
70
|
+
|
71
|
+
The new style, likely to take over in `v1.0.0` after testing:
|
72
|
+
|
73
|
+
```ruby
|
74
|
+
name_longer_than_three = -> person { person.name.size > 3 }
|
75
|
+
people_with_truncated_names = people.map(&Qo.match { |m|
|
76
|
+
m.when(name_longer_than_three) { |person| Person.new(person.name[0..2], person.age) }
|
77
|
+
m.else(&:itself)
|
78
|
+
})
|
79
|
+
|
80
|
+
# And standalone like a case:
|
81
|
+
Qo.match(people.first) { |m|
|
82
|
+
m.when(age: 10..19) { |person| "#{person.name} is a teen that's #{person.age} years old" }
|
83
|
+
m.else { |person| "#{person.name} is #{person.age} years old" }
|
84
|
+
}
|
85
|
+
```
|
86
|
+
|
87
|
+
(More details coming on the difference and planned 1.0.0 APIs)
|
62
88
|
|
63
89
|
### Qo'isms
|
64
90
|
|
data/Rakefile
CHANGED
@@ -44,7 +44,7 @@ def xrun_benchmark(title, **benchmarks) end
|
|
44
44
|
# be readability first with performance coming later. That means that early iterations
|
45
45
|
# may well be slower, but the net expressiveness we get is worth it in the short run.
|
46
46
|
task :perf do
|
47
|
-
puts "Running on Qo v#{Qo::VERSION} at
|
47
|
+
puts "Running on Qo v#{Qo::VERSION} at rev #{`git rev-parse HEAD`} - Ruby #{`ruby -v`}"
|
48
48
|
|
49
49
|
# Compare simple array equality. I almost think this isn't fair to Qo considering
|
50
50
|
# no sane dev should use it for literal 1 to 1 matches like this.
|
data/lib/qo.rb
CHANGED
data/lib/qo/exceptions.rb
CHANGED
@@ -39,5 +39,16 @@ module Qo
|
|
39
39
|
"defined with `Qo.matcher` or `Qo.m` instead of regular matchers."
|
40
40
|
end
|
41
41
|
end
|
42
|
+
|
43
|
+
# In the case of a Pattern Match, we should only have one "else" clause
|
44
|
+
#
|
45
|
+
# @author baweaver
|
46
|
+
# @since 0.3.0
|
47
|
+
#
|
48
|
+
class MultipleElseClauses < ArgumentError
|
49
|
+
def to_s
|
50
|
+
"Cannot have more than one `else` clause in a Pattern Match."
|
51
|
+
end
|
52
|
+
end
|
42
53
|
end
|
43
54
|
end
|
@@ -0,0 +1,115 @@
|
|
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
|
data/lib/qo/public_api.rb
CHANGED
@@ -82,12 +82,32 @@ module Qo
|
|
82
82
|
# it finds one that "matches". Once found, it will pass the target into the
|
83
83
|
# associated matcher's block function.
|
84
84
|
#
|
85
|
+
# @param fn [Proc]
|
86
|
+
# If provided, the pattern match will become block-style, utilizing
|
87
|
+
# PatternMatchBlock instead. If any args are provided, the first
|
88
|
+
# will be treated as the target.
|
89
|
+
#
|
85
90
|
# @param *args [Array[Any, *GuardBlockMatcher]]
|
86
91
|
# Collection of matchers to run, potentially prefixed by a target object
|
87
92
|
#
|
88
|
-
# @return [Qo::
|
89
|
-
#
|
90
|
-
|
93
|
+
# @return [Qo::PatternMatchBlock]
|
94
|
+
# If a value is not provided, a block style pattern match will be returned
|
95
|
+
# that responds to proc coercion. It can be used for functions like `map`.
|
96
|
+
#
|
97
|
+
# @return [Qo::PatternMatch]
|
98
|
+
# If a value is not provided and no function is present, a PatternMatch
|
99
|
+
# will be returned, awaiting a value to match against.
|
100
|
+
#
|
101
|
+
# @return [Any]
|
102
|
+
# If a value is provided, matchers will attempt to call through on it,
|
103
|
+
# returning the result of the function.
|
104
|
+
def match(*args, &fn)
|
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
|
+
|
91
111
|
if args.first.is_a?(Qo::Matchers::GuardBlockMatcher)
|
92
112
|
Qo::Matchers::PatternMatch.new(*args)
|
93
113
|
else
|
data/lib/qo/version.rb
CHANGED
data/performance_report.txt
CHANGED
@@ -1,4 +1,5 @@
|
|
1
|
-
Running on Qo v0.
|
1
|
+
Running on Qo v0.3.0 at rev 77eb2544c41b33361a561178b145d33fa2409bde
|
2
|
+
- Ruby ruby 2.5.1p57 (2018-03-29 revision 63029) [x86_64-darwin17]
|
2
3
|
|
3
4
|
Array * Array - Literal
|
4
5
|
=======================
|
@@ -7,15 +8,15 @@ Vanilla result: true
|
|
7
8
|
Qo.and result: true
|
8
9
|
|
9
10
|
Warming up --------------------------------------
|
10
|
-
Vanilla
|
11
|
-
Qo.and
|
11
|
+
Vanilla 275.791k i/100ms
|
12
|
+
Qo.and 77.679k i/100ms
|
12
13
|
Calculating -------------------------------------
|
13
|
-
Vanilla 8.
|
14
|
-
Qo.and
|
14
|
+
Vanilla 8.805M (± 1.6%) i/s - 44.127M in 5.012793s
|
15
|
+
Qo.and 987.041k (± 2.8%) i/s - 4.971M in 5.040741s
|
15
16
|
|
16
17
|
Comparison:
|
17
|
-
Vanilla:
|
18
|
-
Qo.and:
|
18
|
+
Vanilla: 8805260.8 i/s
|
19
|
+
Qo.and: 987041.0 i/s - 8.92x slower
|
19
20
|
|
20
21
|
|
21
22
|
Array * Array - Index pattern match
|
@@ -25,15 +26,15 @@ Vanilla result: true
|
|
25
26
|
Qo.and result: true
|
26
27
|
|
27
28
|
Warming up --------------------------------------
|
28
|
-
Vanilla
|
29
|
-
Qo.and
|
29
|
+
Vanilla 45.347k i/100ms
|
30
|
+
Qo.and 21.225k i/100ms
|
30
31
|
Calculating -------------------------------------
|
31
|
-
Vanilla
|
32
|
-
Qo.and
|
32
|
+
Vanilla 550.096k (± 2.4%) i/s - 2.766M in 5.031463s
|
33
|
+
Qo.and 226.391k (± 4.2%) i/s - 1.146M in 5.072495s
|
33
34
|
|
34
35
|
Comparison:
|
35
|
-
Vanilla:
|
36
|
-
Qo.and:
|
36
|
+
Vanilla: 550096.4 i/s
|
37
|
+
Qo.and: 226391.4 i/s - 2.43x slower
|
37
38
|
|
38
39
|
|
39
40
|
Array * Object - Predicate match
|
@@ -43,15 +44,15 @@ Vanilla result: false
|
|
43
44
|
Qo.and result: false
|
44
45
|
|
45
46
|
Warming up --------------------------------------
|
46
|
-
Vanilla
|
47
|
-
Qo.and 27.
|
47
|
+
Vanilla 139.508k i/100ms
|
48
|
+
Qo.and 27.451k i/100ms
|
48
49
|
Calculating -------------------------------------
|
49
|
-
Vanilla 2.
|
50
|
-
Qo.and
|
50
|
+
Vanilla 2.148M (± 3.3%) i/s - 10.742M in 5.007009s
|
51
|
+
Qo.and 303.118k (± 2.0%) i/s - 1.537M in 5.073596s
|
51
52
|
|
52
53
|
Comparison:
|
53
|
-
Vanilla:
|
54
|
-
Qo.and:
|
54
|
+
Vanilla: 2147916.4 i/s
|
55
|
+
Qo.and: 303118.1 i/s - 7.09x slower
|
55
56
|
|
56
57
|
|
57
58
|
Array * Array - Select index pattern match
|
@@ -61,15 +62,15 @@ Vanilla result: [["Robert", 22], ["Roberta", 22]]
|
|
61
62
|
Qo.and result: [["Robert", 22], ["Roberta", 22]]
|
62
63
|
|
63
64
|
Warming up --------------------------------------
|
64
|
-
Vanilla
|
65
|
-
Qo.and 7.
|
65
|
+
Vanilla 12.651k i/100ms
|
66
|
+
Qo.and 7.024k i/100ms
|
66
67
|
Calculating -------------------------------------
|
67
|
-
Vanilla
|
68
|
-
Qo.and
|
68
|
+
Vanilla 141.337k (± 8.3%) i/s - 708.456k in 5.067441s
|
69
|
+
Qo.and 73.799k (± 2.9%) i/s - 372.272k in 5.048946s
|
69
70
|
|
70
71
|
Comparison:
|
71
|
-
Vanilla:
|
72
|
-
Qo.and:
|
72
|
+
Vanilla: 141336.5 i/s
|
73
|
+
Qo.and: 73799.3 i/s - 1.92x slower
|
73
74
|
|
74
75
|
|
75
76
|
Hash * Hash - Hash intersection
|
@@ -79,15 +80,15 @@ Vanilla result: [{:name=>"Robert", :age=>22}, {:name=>"Roberta", :age=>22}]
|
|
79
80
|
Qo.and result: [{:name=>"Robert", :age=>22}, {:name=>"Roberta", :age=>22}]
|
80
81
|
|
81
82
|
Warming up --------------------------------------
|
82
|
-
Vanilla
|
83
|
-
Qo.and
|
83
|
+
Vanilla 34.600k i/100ms
|
84
|
+
Qo.and 4.851k i/100ms
|
84
85
|
Calculating -------------------------------------
|
85
|
-
Vanilla
|
86
|
-
Qo.and
|
86
|
+
Vanilla 392.298k (± 2.6%) i/s - 1.972M in 5.030828s
|
87
|
+
Qo.and 48.213k (± 4.8%) i/s - 242.550k in 5.043068s
|
87
88
|
|
88
89
|
Comparison:
|
89
|
-
Vanilla:
|
90
|
-
Qo.and:
|
90
|
+
Vanilla: 392298.2 i/s
|
91
|
+
Qo.and: 48213.1 i/s - 8.14x slower
|
91
92
|
|
92
93
|
|
93
94
|
Hash * Object - Property match
|
@@ -97,13 +98,13 @@ Vanilla result: [#<struct Person name="Robert", age=22>, #<struct Person name="R
|
|
97
98
|
Qo.and result: [#<struct Person name="Robert", age=22>, #<struct Person name="Roberta", age=22>]
|
98
99
|
|
99
100
|
Warming up --------------------------------------
|
100
|
-
Vanilla
|
101
|
-
Qo.and
|
101
|
+
Vanilla 32.783k i/100ms
|
102
|
+
Qo.and 4.422k i/100ms
|
102
103
|
Calculating -------------------------------------
|
103
|
-
Vanilla
|
104
|
-
Qo.and
|
104
|
+
Vanilla 361.140k (± 9.7%) i/s - 1.803M in 5.053763s
|
105
|
+
Qo.and 46.667k (±10.8%) i/s - 229.944k in 5.002258s
|
105
106
|
|
106
107
|
Comparison:
|
107
|
-
Vanilla:
|
108
|
-
Qo.and:
|
108
|
+
Vanilla: 361140.4 i/s
|
109
|
+
Qo.and: 46667.1 i/s - 7.74x slower
|
109
110
|
|
data/qo.gemspec
CHANGED
@@ -10,7 +10,7 @@ Gem::Specification.new do |spec|
|
|
10
10
|
spec.email = ["keystonelemur@gmail.com"]
|
11
11
|
|
12
12
|
spec.summary = %q{Qo is a querying library for Ruby pattern matching}
|
13
|
-
spec.homepage = "https://www.github.com/baweaver/
|
13
|
+
spec.homepage = "https://www.github.com/baweaver/qo"
|
14
14
|
spec.license = "MIT"
|
15
15
|
|
16
16
|
spec.files = `git ls-files -z`.split("\x0").reject do |f|
|
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.3.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-04-
|
11
|
+
date: 2018-04-26 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -182,11 +182,12 @@ files:
|
|
182
182
|
- lib/qo/matchers/guard_block_matcher.rb
|
183
183
|
- lib/qo/matchers/hash_matcher.rb
|
184
184
|
- lib/qo/matchers/pattern_match.rb
|
185
|
+
- lib/qo/matchers/pattern_match_block.rb
|
185
186
|
- lib/qo/public_api.rb
|
186
187
|
- lib/qo/version.rb
|
187
188
|
- performance_report.txt
|
188
189
|
- qo.gemspec
|
189
|
-
homepage: https://www.github.com/baweaver/
|
190
|
+
homepage: https://www.github.com/baweaver/qo
|
190
191
|
licenses:
|
191
192
|
- MIT
|
192
193
|
metadata: {}
|