Computer >> Hướng Dẫn Máy Tính >  >> Lập Trình >> Ruby

Làm Chủ Pattern Matching Ruby: Hướng Dẫn Thực Tế

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:

  1. Match against expected data type
  2. Match against expected hash structure (presence specific keys)
  3. Match against expected array length
  4. 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:

  1. Strong type match: in [Integer => a] hoặc in {a: Integer => a}
  2. Without type: in [a, 1, 2] hoặc in {a: a}
  3. Without variable name, default to key name: in {a:} định nghĩa variable a với value at key a
  4. Bind rest: in [Integer, *rest] hoặc in {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!