Qo - Query Object - Ruby 中的模式匹配和流畅查询
Short for Query Object, my play at Ruby pattern matching and fluent querying, pronounced "Q-whoah".
Read the Docs for more detailed information
Mostly by using Ruby language features like to_proc and ===.
There's an article explaining most of the base mechanics behind Qo:
For Want of Pattern Matching in Ruby - The Creation of Qo
Most of it, though, utilizes Triple Equals. If you're not familiar with what all you can do with it in Ruby, I would encourage you to read this article as well:
The original inspiration was from a chat I'd had with a few other Rubyists about pattern matching, which led to this experiment:
Fast forward a few months and I kind of wanted to make it real, so here it is. Introducing Qo!
Note that Qo uses the Any gem for wildcard matching. Any will respond true to any == or === query against it,
and is included in the gem.
Qo is used for pattern matching in Ruby. All Qo matchers respond to === and to_proc meaning they can be used with case and Enumerable functions alike:
case ['Foo', 42]
when Qo[Any, 42] then 'Truly the one answer'
else nil
end
# Run a select like an AR query, getting the age attribute against a range
people.select(&Qo[age: 18..30])How about some pattern matching? There are two styles:
Qo case statements work much like a Ruby case statement, except in that they leverage the full power of Qo matchers behind the scenes.
# How about some "right-hand assignment" pattern matching
name_longer_than_three = -> person { person.name.size > 3 }
person_with_truncated_name = Qo.case(people.first) { |m|
m.when(name_longer_than_three) { |person|
Person.new(person.name[0..2], person.age)
}
m.else
}It takes in a value directly, and returns the result, much like a case statement.
Note that if else receives no block, it will default to an identity function
({ |v| v }). If no else is provided and there's no match, you'll get back a nil.
You can write this out if you wish.
Match statements are like case statements, except in that they don't directly take a value to match against. They're waiting for a value to come in later from something else.
name_longer_than_three = -> person { person.name.size > 3 }
people_with_truncated_names = people.map(&Qo.match { |m|
m.when(name_longer_than_three) { |person| Person.new(person.name[0..2], person.age) }
m.else
})
# And standalone like a case:
Qo.match { |m|
m.when(age: 10..19) { |person| "#{person.name} is a teen that's #{person.age} years old" }
m.else { |person| "#{person.name} is #{person.age} years old" }
}.call(people.first)Qo supports three main types of queries: and, or, and not.
Most examples are written in terms of and and its alias []. [] is mostly used for portable syntax:
Qo[/Rob/, 22]
# ...is functionally the same as an and query, which uses `all?` to match
Qo.and(/Rob/, 22)
# This is shorthand for
Qo::Matchers::BaseMatcher.new('and', /Rob/, 22)
# An `or` matcher uses the same shorthand as `and` but uses `any?` behind the scenes instead:
Qo.or(/Rob/, 22)
# Same with not, except it uses `none?`
Qo.not(/Rob/, 22)Qo has a few Qo'isms, mainly based around triple equals in Ruby. See the above articles for tutorials on that count.
We will assume the following data:
people_arrays = [
['Robert', 22],
['Roberta', 22],
['Foo', 42],
['Bar', 18]
]
people_objects = [
Person.new('Robert', 22),
Person.new('Roberta', 22),
Person.new('Foo', 42),
Person.new('Bar', 17),
]Qo has a concept of a Wildcard, Any, which will match against any value
Qo[Any, Any] === ['Robert', 22] # trueA single wildcard will match anything, and can frequently be used as an always true:
Qo[Any] === :literally_anything_hereThe first way a Qo matcher can be defined is by using *varargs:
Qo::Matchers::BaseMatcher(type, *varargs, **kwargs)This gives us the and matcher shorthand for array matchers.
When an Array matcher is run against an Array, it will compare elements by index in the following priority:
===)?This functionality is left biased and permissive, meaning that if the right side of the argument is longer it will ignore those items in the match. If it's shorter? Not so much.
We've seen some case matching so far with Range and Regex:
# Standalone
Qo[/Rob/, Any] === ['Robert', 22]
# => true
# Case statement
case ['Roberta', 22]
when Qo[Any, 0..9] then 'child'
when Qo[Any, 10..19] then 'teen'
when Qo[Any, 20..99] then 'adult'
else 'not sure'
end
# => 'adult'
# Select
people_arrays.select(&Qo[Any, 10..19])
# => [['Bar', 18]]If no case match is found, it will attempt to see if a predicate method by the same name exists, call it, and check the result:
dirty_values = [nil, '', true]
# Standalone
Qo[:nil?] === [nil]
# => true, though you could also just use Qo[nil]
# Case statement
case ['Roberta', nil]
when Qo[Any, :nil?] then 'no age'
else 'not sure'
end
# => 'no age'
# Select
people_arrays.select(&Qo[Any, :even?])
# => [["Robert", 22], ["Roberta", 22], ["Foo", 42], ["Bar", 18]]When an Array matcher is matched against anything other than an Array it will follow the priority:
===)?Every argument provided will be run against the target object.
# Standalone
Qo[Integer, 15..25] === 20
# => true
# Case statement - functionally indistinguishable from a regular case statement
# Select
[nil, '', 10, 'string'].select(&Qo.or(/str/, 10..20))
# => [10, "string"]Now this is where some of the fun starts in
# Standalone
Qo.or(:nil?, :empty?) === nil
# => true
Qo.not(:nil?, :empty?) === nil
# => false
# Case statement
case 42
when Qo[Integer, :even?, 40..50] then 'oddly specific number criteria'
else 'nope'
end
# => "oddly specific number criteria"
# Reject
[nil, '', 10, 'string'].reject(&Qo.or(:nil?, :empty?))
# => [10, "string"]Checks to see if the key is even present on the other object, false if not.
If both the match value (match_key: matcher) and the match target are hashes, Qo will begin a recursive descent starting at the match key until it finds a matcher to try out:
Qo[a: {b: {c: 5..15}}] === {a: {b: {c: 10}}}
# => true
# Na, no fun. Deeper!
Qo.and(a: {
f: 5..15,
b: {
c: /foo/,
d: 10..30
}
}).call(a: {
f: 10,
b: {
c: 'foobar',
d: 20
}
})
# => true
# It can get chaotic with `or` though. Anything anywhere in there matches and
# it'll pass.
Qo.or(a: {
f: false,
b: {
c: /nope/,
d: 10..30
}
}).call(a: {
f: 10,
b: {
c: 'foobar',
d: 20
}
})If a case match is present for the key, it'll try and compare:
# Standalone
Qo[name: /Foo/] === {name: 'Foo'}
# => true
# Case statement
case {name: 'Foo', age: 42}
when Qo[age: 40..50] then 'Gotcha!'
else 'nope'
end
# => "Gotcha!"
# Select
people_hashes = people_arrays.map { |n, a| {name: n, age: a} }
people_hashes.select(&Qo[age: 15..25])
# => [{:name=>"Robert", :age=>22}, {:name=>"Roberta", :age=>22}, {:name=>"Bar", :age=>18}]Much like our array friend above, if a predicate style method is present see if it'll work
# Standalone
Qo[name: :empty?] === {name: ''}
# => true
# Case statement
case {name: 'Foo', age: nil}
when Qo[age: :nil?] then 'No age provided!'
else 'nope'
end
# => "No age provided!"
# Reject
people_hashes = people_arrays.map { |(n,a)| {name: n, age: a} } [{:name=>"Robert", :age=>22}, {:name=>"Roberta", :age=>22}, {:name=>"Bar", :age=>18}]Careful though, if the key doesn't exist that won't match. I'll have to consider this one later.
Coerces the key into a string if possible, and sees if that can provide a valid case match
If it doesn't know how to deal with it, false out.
This is where we can get into some interesting code, much like the hash selections above
# Standalone
Qo[name: /Rob/] === people_objects.first
# => true
# Case statement
case people_objects.first
when Qo[name: /Rob/] then "It's Rob!"
else 'Na, not them'
end
# => "It's Rob!"
# Select
people_objects.select(&Qo[name: /Rob/])
# => [Person(Robert, 22), Person(Roberta, 22)]# Standalone
Qo[name: :empty?] === Person.new('', 22)
# => true
# Case statement
case Person.new('', nil)
when Qo[age: :nil?] then 'No age provided!'
else 'nope'
end
# => "No age provided!"
# Select
people_hashes.select(&Qo[age: :nil?])
# => []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.
Qo.case(['Robert', 22]) { |m|
m.when(Any, 20..99) { |n, a| "#{n} is an adult that is #{a} years old" }
m.else
}
# => "Robert is an adult that is 22 years old"Qo.case(people_objects.first) { |m|
m.when(name: Any, age: 20..99) { |person| "#{person.name} is an adult that is #{person.age} years old" }
m.else
}In this case it's trying to do a few things:
If no block function is provided, it assumes an identity function (-> v { v }) instead. If no match is found, nil will be returned.
name_longer_than_three = -> person { person.name.size > 3 }
people_objects.map(&Qo.match { |m|
m.when(name_longer_than_three) { |person| Person.new(person.name[0..2], person.age) }
m.else
})
# => [Person(age: 22, name: "Rob"), Person(age: 22, name: "Rob"), Person(age: 42, name: "Foo"), Person(age: 17, name: "Bar")]So we just truncated everyone's name that was longer than three characters.
There are a few functions added for convenience, and it should be noted that because all Qo matchers respond to === that they can be used as helpers as well.
Dig is used to get in deep at a nested hash value. It takes a dot-path and a === respondent matcher:
Qo.dig('a.b.c', Qo.or(1..5, 15..25)) === {a: {b: {c: 1}}}
# => true
Qo.dig('a.b.c', Qo.or(1..5, 15..25)) === {a: {b: {c: 20}}}
# => trueTo be fair that means anything that c
暂无开放 Issues,或尚未同步最近议题。