เริ่มด้วยการพูดคุยสั้นๆ เกี่ยว pattern matching ใน Ruby คืออะไร ทำอะไร และช่วย improve code readability อย่างไร
ถ้าคุณแบบผมเมื่อปีก่อน อาจสับสนกับ pattern matching ใน Regex Google 'pattern matching' โดยไม่มี context อื่นๆ ก็เจอ content ใกล้เคียงนิยามนั้น
Formal: pattern matching = process of checking any data (sequence of characters, tokens, tuple, anything) against other data
Programming terms: ขึ้นกับ capabilities ของภาษา อาจหมายถึง:
- Match against expected data type
- Match against expected hash structure (เช่น presence of specific keys)
- Match against expected array length
- Assign matches (หรือส่วนหนึ่ง) ให้ variables
Pattern matching แรกของผมผ่าน Elixir Elixir มี first-class support มากจน = operator คือ match operator ไม่ใช่ simple assignment
หมายถึงใน Elixir นี้ valid code:
iex> x = 1 iex> 1 = x With that in mind มาดู pattern matching support ใหม่ใน Ruby 2.7+ และใช้ทำ code readable มากขึ้น ตั้งแต่วันนี้
Ruby Pattern Matching ด้วย case/in
Ruby support pattern matching ด้วย special case/in expression Syntax:
case in # ... in # ... else # ... end ไม่สับสนกับ case/when when และ in branches ไม่ผสมใน case เดียวได้
ไม่ provide else → failing match จะ raise NoMatchingPatternError
Pattern Matching Arrays ใน Ruby
Pattern matching ใช้ match arrays กับ pre-required structures: data types, lengths, values
ตัวอย่าง matches ทั้งหมด (note: แค่ in แรก evaluate เพราะ case stop หลัง first match):
case [1, 2, "Three"] in [Integer, Integer, String] "matches" in [1, 2, "Three"] "matches" in [Integer, *] "matches" # เพราะ * = spread operator match อะไรก็ได้ in [a, *] "matches" # และ variable a = 1 end Pattern matching clause นี้ useful ตอนอยาก produce multiple signals จาก method call
ใน Elixir world ใช้บ่อยตอน operations ที่มีทั้ง :ok และ :error results เช่น insert database
ใช้เพื่อ readability:
def create case save(model_params) in [:ok, model] render :json => model in [:error, errors] render :json => errors end end # ที่ไหนก็ได้ในโค้ด เช่น global helper หรือ model base class (ชื่อต่างกัน) def save(attrs) model = Model.new(attrs) model.save ? [:ok, model] : [:error, model.errors] end Pattern Matching Objects ใน Ruby
Match objects ใน Ruby เพื่อ enforce specific structure:
case {a: 1, b: 2} in {a: Integer} "matches" # By default, all object matches are 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 เฉพาะ object มี a และไม่มี keys อื่น end ใช้ได้ดีตอน impose strong rules match params
เช่น fancy greeter อาจมี 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 และ Pinning ใน Ruby
เห็นในตัวอย่างข้างต้น pattern matching useful assign 部分ของ patterns ให้ arbitrary variables เรียกว่า variable binding มีหลายวิธี bind variable:
- Strong type match:
in [Integer => a]หรือin {a: Integer => a} - Without type specification:
in [a, 1, 2]หรือin {a: a} - Without variable name: default ใช้ key name
in {a:}define variableavalue at keya - Bind rest:
in [Integer, *rest]หรือin {a: Integer, **rest}
Match ตอนอยากใช้ existing variable เป็น sub-pattern? ใช้ variable pinning ด้วย ^ (pin) operator:
a = 1 case {a: 1, b: 2} in {a: ^a} "matches" end ใช้ได้แม้ variable defined ใน pattern เอง เขียน 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 สำคัญ: แม้ pattern ไม่ fully match variable ยัง bound แล้ว useful บางครั้ง
แต่ส่วนใหญ่อาจ subtle bugs → อย่า rely on shadowed variable values ที่ใช้ใน match เช่น ตัวนี้คาดว่า city = "Amsterdam" แต่ได้ "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 แทน Amsterdam Matching Ruby Custom Classes
Implement special methods ทำ custom classes pattern matching aware
เช่น pattern match user ตาม first_name และ last_name define deconstruct_keys บน 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 ของ deconstruct_keys มี keys ที่ requested ใน pattern receiver ให้แค่ required keys ถ้า compute ทั้งหมด expensive
เหมือน deconstruct_keys implement deconstruct ให้ objects pattern matched as array เช่น Location class มี latitude longitude นอกจาก deconstruct_keys ให้ keys แล้ว expose array [latitude, longitude]:
class Location def deconstruct [latitude, longitude] end end case location in [Float => latitude, Float => longitude] puts "#{latitude}, #{longitude}" end ใช้ Guards สำหรับ Complex Patterns
Complex patterns ที่ regular pattern match operators ไม่ represent ได้ ใช้ if (หรือ unless) statement เป็น guard:
case [1, 2] in [a, b] if b == a * 2 "matches" else "no match" end Pattern Matching ด้วย =>/in โดยไม่มี case
Ruby 3+ มี pattern matching magic เพิ่ม single line without 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] Syntax นี้ไม่มี else clause useful ตอน data structure รู้จักล่วงหน้า
ตัวอย่าง: pattern นี้ fit ใน 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 ใน Ruby: Watch This Space
Pattern matching อาจแปลกตอนแรก 覺得像 glorified object/array deconstruction
แต่ถ้า popularity ของ Elixir บ่งบอก pattern matching = great tool ใน arsenal First-hand experience บน Elixir ยืนยันว่ายากที่จะขาดมันเมื่อคุ้นเคย
Ruby 2.7: pattern matching (case/in) ยัง experimental Ruby 3: case/in stable single-line pattern matching expressions experimental ปิด warnings: Warning[:experimental] = false ในโค้ด หรือ -W:no-experimental command-line
แม้ pattern matching ใน Ruby ยัง early stages หวังว่า introduction นี้ useful และคุณ excited กับ future developments เหมือนผม!
P.S. อยากอ่าน Ruby Magic posts ทันทีเผยแพร่? สมัครรับ Ruby Magic newsletter!