ทำไมผมถึงชอบ Elixir |> Pattern Matching
สวัสดีครับ สำหรับบทความนี้จะไม่เกี่ยวกับซีรีย์ “สร้าง 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 ที่แตกต่าง ดังนี้
- play(min, max) จะรับค่า min, max เข้ามาแล้วสร้าง list ทั้งแต่ min จนถึง max จากนั้น loop ค่าแค่ละตัวเข้าไปใส่ในฟังชั่น play ที่รับ 1 arity ก็คือ play(num)
- play(num) ฟังชั่นที่จะทำงานเมื่อ num หาร 15 ลงตัว (แสดงคำว่า “FizzBuzz”)
- play(num) ฟังชั่นที่จะทำงานเมื่อ num หาร 3 ลงตัว (แสดงคำว่า “Fizz”)
- play(num) ฟังชั่นที่จะทำงานเมื่อ num หาร 5 ลงตัว (แสดงคำว่า “Buzz”)
- play(num) ฟังชั่นที่จะทำงานเมื่อไม่เข้า Clause ด้านบน (แสดงค่าของ num)
ว่าแล้วก็ลองเรียกใช้ฟังชั่น play(min, max) ดูเลยดีกว่า โดยให้เริ่มจาก 0 ถึง 20 และผลลัพธ์ก็ออกมาอย่างที่เห็นในภาพด้านล่างนี้เลยครับ
บทสรุป — Elixir |> Pattern Matching
จากที่ยกตัวอย่างมาบางส่วนแล้ว ทุกคนก็คงพอจะเห็นภาพว่าตัว Pattern Matching เองมีประโยชน์มากแค่ไหน ซึ่งจริงๆแล้ว มันสามารถเอาไปใช้งานได้หลากหลายรูปแบบมาก เพียงแค่ผมเองยังไม่มีโอกาสได้หยิบมานำเสนอให้กับทุกคนได้เห็นกัน ผมมั่นใจว่าในอนาคตถ้าเพื่อนๆได้ลองสัมผัสกันมันด้วยตัวเองแล้ว รับรองว่าจะชอบอย่างแน่นอน แล้วจะคิดถึงมันเมื่อเปลี่ยนไปเขียนภาษาอื่น :)
แล้วพบกันใหม่ครับ — SukinoSenze