qo 0.2.1 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- 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: {}
|