ทำไมผมถึงชอบ Elixir |> Pattern Matching

Pongsatorn Paewsoongnern
fastwork
Published in
3 min readJul 25, 2018

สวัสดีครับ สำหรับบทความนี้จะไม่เกี่ยวกับซีรีย์ “สร้าง Web App ด้วย Elixir ❤ Phoenix กันเถอะ” แต่จะมาขายของว่าทำไม ผมถึงชอบ Elixir โดยเฉพาะตัว Pattern Matching ที่ค่อนข้างจะได้ใจผมไปมากเลยทีเดียว

Pattern Matching เป็นสิ่งหนึ่งที่ผมคิดถึงมาเวลาเขียน Go เพราะมันไม่มีให้ใช้ T_T น้ำตาจิไหล // เข้าใจแหละว่า Purpose ของภาษามันต่างกัน

อะไรคือ Pattern Matching กันนะ?

อ้างอิงจาก: Understanding Pattern Matching in Elixir — culttt.com

Pattern Matching คือว่าเป็นจุดเด่นที่สุดจุดหนึ่งของ Elixir เลยก็ว่าได้ มันทำให้ผมสนใจในตัวภาษาและลองเอาไปใช้งานต่างๆดู

ถ้าจะให้ผมพูดคงได้ประมาณว่า Pattern Matching ได้เปิดโลกใหม่ในการเขียนโปรแกรมให้กับผม อารมณ์แบบ อ้าวเขียนแบบนี้ก็ได้หรอ!!!!

เพราะก่อนหน้านี้ผมก็ศึกษาภาษาฝั่ง Functional Programming มาบ้างอย่าง Scala และ F# แต่บอกเลยว่าตัว Pattern Matching ของ Elixir นั้นต่างออกไปอย่างสิ้นเชิง มันจะเป็นยังไงเดี๋ยวมาดูกัน

มองดูเผินๆแล้ว ทุกคนคงคิดว่ามันคือ = (assignment) ปกติทั่วไป แต่มันไม่ใช่เลย มันคือการ match variable กับ value ต่างหาก

anime = "Sword Art Online"

เมื่อ Statement ข้างต้นถูกเรียกใช้ มันจะเป็นการ match ระหว่าง Pattern ที่อยู่ทางซ้าย กับ Expression ที่อยู่ทางขวา ทำให้ anime มีค่าเป็น “Sword Art Online”

พอบอกไปแบบนี้ก็จะ งง กันอีกละ ไม่เป็นไรไปต่อ!

สมมติว่าผมมี Tuple 2 ตัวหนึ่ง มี String อยู่ข้างในประมาณว่า {"Hello", "World"} ถ้าเกิดผมทำแบบนี้ คิดว่าจะเกิดอะไรขึ้น?

{some, thing} = {"Hello", "World"}

ก็คงจะเดาได้ว่า some จะมีค่าเป็น “Hello” และ thing จะมีค่าเป็น “World” นั่นเอง

แต่ถ้าด้านซ้ายมือผมใส่แค่ some อย่างเดียวละ? ก็จะเจอกับ MatchError ครับ เนื่องจากตัว Pattern Matching ไม่สามารถ Match ได้เพราะซ้ายมือเป็น Tuple 1 แต่ ขวามือเป็น tuple 2 นั่นเอง

{some} = {"Hello", "World"}** (MatchError) no match of right hand side value: {"Hello", "World"}

แล้วส่วนใหญ่เอาไปใช้กันตรงไหนนะ?

จริงๆแล้ว Pattern Matching ถูกใช้ในหลายส่วนมากในภาษา Elixir แต่ที่เห็นได้ชัดที่สุดก็คงจะเห็นในส่วนของ Return value จากการเรียกใช้ฟังชั่นต่างๆ

File.read("anime.txt")
{:ok, "Sword Art Online\n"}
File.read("not_exist.txt")
{:error, :enoent}

เวลาผมเรียกฟังชั่น File.Read ถ้าไฟล์มีอยู่จริงก็จะได้รับ Response เป็น {:ok, value} แต่ถ้าเกิดไม่สามารถอ่านไฟล์ได้ ก็จะได้เป็น {:error, reason} ซึ่งสามารถเอาไปประยุกต์ใช้ได้ประมาณนี้

เห็นไหมละครับ ว่าเอาไปใช้ได้ง่ายมาก แถมยังทำให้โค๊ดเราสวยงามอีกด้วย :)

อ๊ะ ยังไม่จบ ต่อกันอีกหน่อย

# ตัวอย่าง 1
%{studio: studio} = %{name: "SAO", studio: "A-1", episodes: 24}
# ตัวอย่าง 2
[one, two, three] = [1, 2, 3]
# ตัวอย่าง 3
[head | tail] = [1, 2, 3]
# ตัวอย่าง 4
{_, second, _} = {"hello", "world", "jelly"}

ตัวอย่าง 1

studio จะมีค่าเป็น “A-1”

ตัวอย่าง 2

one, two และ three จะมีค่าเป็น 1, 2 และ 3 ตามลำดับ

ตัวอย่าง 3

head จะมีค่าเป็น 1 ส่วน tail จะมีค่าเป็น [2,3]

ตัวอย่าง 4

second จะมีค่าเป็น “world” ซึ่งเราไม่สนใจว่า 1 และ 3 จะเป็นอะไร โดยใช้ _

ตัวอย่างการใช้งานกับฟังชั่น

เราจะเริ่มดูกันไปทีละส่วนนะครับ เริ่มจากฟังชั่น start() ก่อน ในฟังชั่นนี้ผมได้ทำการ

  • สร้าง List ของ anime ขึ้นมาโดยประกอบด้วย map ที่มี key name, studio และ episodes อยู่
  • โยน List เข้าไปยังฟังชั่น hello_anime โดยใช้ Pipe Operator |> (เดี๋ยวจะมีแนะนำในบทต่อไป)

ซึ่งคุณสมบัติของฟังชั่น hello คือเป็นฟังชั่นที่รับ [one, two, three | other] = _anime แน่นอนว่าพอพูดแบบนี้ทุกคนก็งงว่ามันคืออะไรกัน?!

มันคือการรับ list ของอะไรก็ได้มา โดย match element ใส่ตัวแปร one, two three ตามลำดับ และให้ elements ส่วนที่เหลือใส่ในตัวแปร other และก็ให้ list ทั้งหมดนี้ใส่ตัวแปร anime ไว้ (สาเหตุที่ผมใส่ _ ไว้ข้างหน้าเพราะต้องการ ignore ตัวแปรนี้ ไม่งั้นมันจะเตือนว่าไม่ได้ใช้ครับ)

เอาหล่ะ มาลองรันกันดูเลยดีกว่า และผลลัพท์ก็ออกมาตามที่เห็นในภาพด้านล่างนี้เลย

จะเห็นได้ว่าตัวแปร one, two และ three จะเป็นค่า map ของ anime ส่วน other เป็น list ของ map อีกทีหนึ่ง

นี่แหละ พลังของ Pattern Matching

ประยุกต์เอาไปใช้กับฟังชั่น (ขั้นกว่า)

อ้างอิงจาก: 7 Lines to Code Fizz-Buzz Game Principle in Elixir

ถ้าจะให้ยกตัวอย่างที่เทพกว่านี้ ก็อยากจะลองยกตัวอย่างการใช้ Pattern Matching กับ Function เพื่อแก้ปัญหา FizzBuzz ยอดนิยม นั่นเอง

โดยในตัวอย่างนี้จะเป็นการใช้ Guards เข้ามาช่วยเพิ่มพลังไปอีกขึ้น
Guards Doc: https://hexdocs.pm/elixir/master/guards.html

จะเห็นได้ว่า เรามีฟังชั่น play อยู่ทั้งหมด 5 ฟังชั่นด้วยกัน โดยแต่ละอันจะมี Signature ที่แตกต่าง ดังนี้

  1. play(min, max) จะรับค่า min, max เข้ามาแล้วสร้าง list ทั้งแต่ min จนถึง max จากนั้น loop ค่าแค่ละตัวเข้าไปใส่ในฟังชั่น play ที่รับ 1 arity ก็คือ play(num)
  2. play(num) ฟังชั่นที่จะทำงานเมื่อ num หาร 15 ลงตัว (แสดงคำว่า “FizzBuzz”)
  3. play(num) ฟังชั่นที่จะทำงานเมื่อ num หาร 3 ลงตัว (แสดงคำว่า “Fizz”)
  4. play(num) ฟังชั่นที่จะทำงานเมื่อ num หาร 5 ลงตัว (แสดงคำว่า “Buzz”)
  5. play(num) ฟังชั่นที่จะทำงานเมื่อไม่เข้า Clause ด้านบน (แสดงค่าของ num)

ว่าแล้วก็ลองเรียกใช้ฟังชั่น play(min, max) ดูเลยดีกว่า โดยให้เริ่มจาก 0 ถึง 20 และผลลัพธ์ก็ออกมาอย่างที่เห็นในภาพด้านล่างนี้เลยครับ

บทสรุป — Elixir |> Pattern Matching

จากที่ยกตัวอย่างมาบางส่วนแล้ว ทุกคนก็คงพอจะเห็นภาพว่าตัว Pattern Matching เองมีประโยชน์มากแค่ไหน ซึ่งจริงๆแล้ว มันสามารถเอาไปใช้งานได้หลากหลายรูปแบบมาก เพียงแค่ผมเองยังไม่มีโอกาสได้หยิบมานำเสนอให้กับทุกคนได้เห็นกัน ผมมั่นใจว่าในอนาคตถ้าเพื่อนๆได้ลองสัมผัสกันมันด้วยตัวเองแล้ว รับรองว่าจะชอบอย่างแน่นอน แล้วจะคิดถึงมันเมื่อเปลี่ยนไปเขียนภาษาอื่น :)

แล้วพบกันใหม่ครับ — SukinoSenze

--

--