Computer >> บทช่วยสอนคอมพิวเตอร์ >  >> การเขียนโปรแกรม >> Ruby

เชี่ยวชาญ Ruby Pattern Matching: คู่มือปฏิบัติจริง

เริ่มด้วยการพูดคุยสั้นๆ เกี่ยว 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 ของภาษา อาจหมายถึง:

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

  1. Strong type match: in [Integer => a] หรือ in {a: Integer => a}
  2. Without type specification: in [a, 1, 2] หรือ in {a: a}
  3. Without variable name: default ใช้ key name in {a:} define variable a value at key a
  4. 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!