Bắt đầu thảo luận ngắn về pattern matching Ruby: nó làm gì, cải thiện code readability như thế nào.
Nếu bạn như tôi vài năm trước, có thể confuse với pattern matching trong Regex. Google 'pattern matching' no context → content close to that definition.
Formal: pattern matching = process checking any data (character sequence, tokens, tuple, anything) against other data.
Programming context, tùy language capabilities, có thể nghĩa:
- Match against expected data type
- Match against expected hash structure (presence specific keys)
- Match against expected array length
- Assign matches (or part) to variables
First foray pattern matching qua Elixir. Elixir first-class support, = operator thực sự là match operator, không phải simple assignment.
Elixir code valid:
iex> x = 1 iex> 1 = x
Với đó, xem pattern matching support Ruby 2.7+ và dùng làm code readable hơn từ hôm nay.
Ruby Pattern Matching Với case/in
Ruby support pattern matching với special case/in expression. Syntax:
case <expression> in <pattern1> # ... in <pattern2> # ... else # ... end
Không confuse với case/when. when và in branches không mix trong single case.
Không provide else → failing match raise NoMatchingPatternError.
Pattern Matching Arrays Trong Ruby
Match arrays đến pre-required structures against data types, lengths, values.
Ví dụ matches (chỉ first in evaluated, case stop sau first match):
case [1, 2, "Three"] in [Integer, Integer, String] "matches" in [1, 2, "Three"] "matches" in [Integer, *] "matches" # * = spread operator match anything in [a, *] "matches" # variable a = 1 end
Pattern matching clause này useful khi want multiple signals từ method call.
Elixir world: dùng khi operations có both :ok và :error result (vd: insert database).
Cách dùng cho readability:
def create
case save(model_params)
in [:ok, model]
render json: model
in [:error, errors]
render json: errors
end
end
# Somewhere in code, e.g. global helper or model base class
def save(attrs)
model = Model.new(attrs)
model.save ? [:ok, model] : [:error, model.errors]
end
Pattern Matching Objects Trong Ruby
Match objects enforce specific structure:
case {a: 1, b: 2}
in {a: Integer}
"matches" # default object matches partial
in {a: Integer, **}
"matches" # same as {a: Integer}
in {a: a}
"matches" # variable a = 1
in {a: Integer => a}
"matches" # variable a = 1
in {a: 1, b: b}
"matches" # variable b = 2
in {a: Integer, **nil}
"does not match" # match only if object has a and NO other keys
end
Great khi impose strong rules matching against params.
Ví dụ fancy greeter với structure strongly opinionated:
def greet(hash = {})
case hash
in {greeting: greeting, first_name: first_name, last_name: last_name}
greet(greeting: greeting, name: "#{first_name} #{last_name}")
in {greeting: greeting, name: name}
puts "#{greeting}, #{name}"
in {name: name}
greet(greeting: "Hello", name: name)
in {greeting: greeting}
greet(greeting: greeting, name: "Anonymous")
else
greet(greeting: "Hello", name: "Anonymous")
end
end
greet # Hello, Anonymous
greet(name: "John") # Hello, John
greet(first_name: "John", last_name: "Doe") # Hello, John Doe
greet(greeting: "Bonjour", first_name: "John", last_name: "Doe") # Bonjour, John Doe
greet(greeting: "Bonjour") # Bonjour, Anonymous
Variable Binding Và Pinning Trong Ruby
Như thấy examples, pattern matching useful assign part patterns to variables. Called variable binding, several ways:
- Strong type match:
in [Integer => a]hoặcin {a: Integer => a} - Without type:
in [a, 1, 2]hoặcin {a: a} - Without variable name, default to key name:
in {a:}định nghĩa variableavới value at keya - Bind rest:
in [Integer, *rest]hoặcin {a: Integer, **rest}
Match khi want dùng existing variable as sub-pattern? Dùng variable pinning với operator ^ (pin):
a = 1
case {a: 1, b: 2}
in {a: ^a}
"matches"
end
Thậm chí dùng khi variable defined trong pattern itself → powerful patterns:
case order
in {billing_address: {city:}, shipping_address: {city: ^city}}
puts "both billing and shipping are to the same city"
else
raise "both billing and shipping must be to the same city"
end
Quirk quan trọng: even pattern không fully match, variable vẫn bound. Có thể useful.
Nhưng most cases cause subtle bugs → đừng rely on shadowed variable values used inside match. Ví dụ, expect city = "Amsterdam" nhưng actual "Berlin":
city = "Amsterdam"
order = {billing_address: {city: "Berlin"}, shipping_address: {city: "Zurich"}}
case order
in {billing_address: {city:}, shipping_address: {city: ^city}}
puts "both billing and shipping are to the same city"
else
puts "both billing and shipping must be to the same city"
end
puts city # Berlin thay vì Amsterdam
Matching Custom Classes Ruby
Implement special methods để custom classes pattern matching aware.
Ví dụ pattern match user against first_name, last_name, define deconstruct_keys trên class:
class User
def deconstruct_keys(keys)
{first_name: first_name, last_name: last_name}
end
end
case user
in {first_name: "John"}
puts "Hey, John"
end
keys argument chứa keys requested trong pattern. Way cho receiver provide chỉ required keys nếu computing all expensive.
Cùng cách deconstruct_keys, implement deconstruct cho objects pattern matched as array. Ví dụ Location class có latitude, longitude. Ngoài deconstruct_keys, expose array [latitude, longitude]:
class Location
def deconstruct
[latitude, longitude]
end
end
case location
in [Float => latitude, Float => longitude]
puts "#{latitude}, #{longitude}"
end
Dùng Guards Cho Complex Patterns
Complex patterns không represent được regular pattern match operators → dùng if (hoặc unless) guard:
case [1, 2] in [a, b] if b == a * 2 "matches" else "no match" end
Pattern Matching Với =>/in Không Cần case
Ruby 3+: single-line pattern matching không cần case statement:
[1, 2, "Three"] => [Integer => one, two, String => three] puts one # 1 puts two # 2 puts three # Three # Same as above [1, 2, "Three"] in [Integer => one, two, String => three]
Không có else clause → useful khi data structure known beforehand.
Ví dụ pattern fit well base controller allow only admin users:
class AdminController < AuthenticatedController
before_action :verify_admin
private
def verify_admin
Current.user => {role: :admin}
rescue NoMatchingPatternError
raise NotAllowedError
end
end
Pattern Matching Trong Ruby: Watch This Space
Đầu tiên pattern matching có thể feel strange. To some, giống glorified object/array deconstruction.
Nhưng nếu popularity Elixir là indication, pattern matching là great tool arsenal. First-hand experience dùng Elixir, confirm hard to live without once used to it.
Ruby 2.7: pattern matching (case/in) experimental. Ruby 3: case/in stable, single-line expressions experimental. Tắt warnings: Warning[:experimental] = false code hoặc -W:no-experimental command-line.
Dù early stages, hy vọng intro useful và bạn excited như tôi về future developments!
P.S. Đăng ký Ruby Magic newsletter để đọc sớm nhất!