Thursday, June 23, 2016

Functor คืออะไร

จากคำถามใน group "Thai Functional Enthusiasts" https://www.facebook.com/groups/310209089128699/ มีคนถามว่า..

ผมขอคำอธิบาย 3 คำนี้หน่อยครับ
Functor, Applicative Functors, Monad รู้สึกยัง งงๆ กับ 3 คำนี้
ตามที่ผมเข้าใจ คือ
1. Functor มันคือการจำกัด type เพื่อจะเช็คว่า type ที่ส่งไปถูกต้องหรือไม่ ถ้าสร้างไว้แต่แรกๆ จะไม่ต้องมาเช็คพวก null, nil อีก
2. Applicative Functors เหมือน Functor แต่ทำกับพวก function 3. Monad นี้ งง สุดเลยถ้าอธิบายแล้ว งง ขออภัยครับ งง เหมือนกัน
อีกคำถามครับ Monad, Monoid, Monadic มันคืออันเดียวกันใช่ไหมครับ

พิมพ์ตอบใน facebook ท่าจะยาวเกิน เลยเขียน blog ครับ เผื่อมีประโยชน์ต่อคนอื่นบ้าง

คือ functor, applicative และ monad เป็น typeclass แปลว่าสิ่งที่มันกำหนดจริง ๆ มีแต่ชื่อ function กับ type ที่ function ไปยุ่ง ซึ่งถือว่า abstract มาก ถ้าเราพยายามหาตัวอย่างที่มัน concrete เราอาจจะยิ่งงง เพราะเราจะไปยึดติดกับของที่มัน concrete ทำให้เอาไปใช้กับอย่างอื่นไม่ได้ ซึ่งผมว่านี่เป็นปัญหายอดฮิตของคนที่เขียนภาษาอื่นก่อนมาหัด haskell เลย

ผมว่าก่อนจะรู้ functor, applicative, monad และ monoid จริง ๆ ต้องรู้เรื่อง value/type/kind และ typeclass ก่อนครับ มันสัมพันธ์กันหมด อธิบายด้วยอะไรที่จับต้องไม่ได้ แต่ผมจะลองอธิบาย functor แบบบ้าน ๆ ดู โดยจะพยายามไม่ให้เสีย concept ครับ

วันนี้ขอเรื่อง functor ก่อน.. (เรื่องอื่นไว้ post หน้าครับ) functor คืออะไร  functor คือ "context ของ <อะไรซักอย่าง> ที่เราสามารถเปลี่ยนไอ้ <อะไรซักอย่าง> เป็น <อะไรอีกอย่าง> ได้ แต่มีข้อแม้ว่า <อะไรอีกอย่าง> ยังต้องอยู่ใน context เดิม" ยิ่งฟังยิ่งงงเนอะ 555 ไม่เป็นไร พักไว้ก่อน

ดู definition ของ Functor บ้าง..

class Functor f where
  fmap :: (a -> b) -> f a -> f b

อ่านออกเสียงเป็นภาษาไทยว่า ถ้า f เป็น context นึง และ f เป็น functor แล้ว เราสามารถแปลง (fmap) มัน โดยวิธีการแปลง จะให้คนอยากแปลงส่ง function 1 ตัว (a -> b) เพื่อเปลี่ยน "อะไร" (a) ใน context f (f a) ให้เป็น "อีกอย่าง" (b) ใน context f (f b) เหมือนเดิม เช่น

ถ้าผมมี "ขวดน้ำที่ใส่อะไรซักอย่าง" และผมรู้ว่าขวดน้ำผมเป็น functor แปลว่าผมสามารถเปลี่ยนของในขวดอย่างนึงไปเป็นอีกอย่างได้แน่นอน เช่น เปลี่ยนน้ำในขวดเป็นน้ำแข็ง แต่น้ำแข็งต้องอยู่ในขวดเหมือนเดิมนะครับ ถ้าบ้าจี้เขียน code ก็จะได้ประมาณนี้

in: fmap (\น้ำ -> แช่แข็ง น้ำ) (ขวด น้ำ)
out: (ขวด น้ำแข็ง)

เอาใหม่ให้มันเบลอ ๆ กว่าเดิม ถ้าผมมี "ความรู้สึกดี ๆ กับอะไรซักอย่าง" และผมรู้ว่า "ความรู้สึกดี ๆ" เป็น functor แปลว่าผมสามารถเปลี่ยนความรู้สึกดี ๆ ต่อนาย ก เป็นความรู้สึกดี ๆ ต่อนาย ข หรืออาจจะเปลี่ยนไปรู้สึกดี ๆ กับหมาข้างถนนก็ได้ จะเห็นว่าผมสามารถเปลี่ยน "อะไร" ที่ผมรู้สึกดี ๆ ต่อได้ แต่ผมไม่สามารถเปลี่ยน context จากความรู้สึกดี ๆ เป็นความรู้สึกเกลียดชังได้

กลับมาตัวอย่างที่ใช้จริง เช่น Maybe .. context ของ Maybe คือ "ความอาจจะมีหรือไม่มีก็ได้" และ Maybe เป็น functor แปลว่าเราสามารถแปลง (fmap) ความอาจจะมีหรือไม่มีอะไรซักอย่าง ให้เป็นความอาจจะมีหรือไม่มีอะไรอีกอย่างได้ เช่น Maybe String (อาจจะมี string) กลายเป็น Maybe Int (อาจจะมีตัวเลข) ถ้าเราจะเปลี่ยนอะไรมัน เราก็เปลี่ยนได้เฉพาะกรณีที่มีค่าเท่านั้น

in: fmap (\s -> length s) (Just "hello")
out: (Just 5)

in: fmap (\s -> length s) (Nothing)
out: (Nothing)

ส่วน list .. context ของมันคือ "ความเป็น collection" หรือ "ความเป็นไปได้หลายอย่าง" การที่เราจะเปลี่ยนอะไร เราก็ไล่เปลี่ยนทีละตัว ถ้าไม่มีอะไรเลย (list ว่าง) เราก็ไม่ต้องเปลี่ยน (คืน list ว่าง)

in: fmap (\n -> show (n * 2)) [1,2,3,4,5]
out: ["2", "4", "6", "8", "10"]

in: fmap (\n -> show (n * 2)) []
out: []

หรือ async (ถ้าไม่เคยใช้ใน haskell นึกถึง promise ของ javascript) .. context ของ async คือ "เดี๋ยวชั้นทำงานเสร็จแล้ว ชั้นจะคืนค่าอะไรซักอย่างให้แก" ถ้าเราจะแปลง async เราก็ต้องแปลงไอ้ค่าที่ async จะคืนมา ตอนมันทำงานเสร็จนั่นเอง

in: data Student = Student { name :: String }
in: do
in:   name <- async (plankFor10MinsAndGetStudentName)    -- เดิม เป็น Async String
in:   let student = fmap (\s -> Student s) name               -- fmap แล้วกลายเป็น Async Student
in:   wait student
out: IO (Student "John")


ทิ้งท้าย.. monad กับ monadic ถ้าไม่ซีเรียส มองว่ามันเหมือนกันก็ได้ครับ ภาษาไทยเราใช้ noun กับ adj ปนกันอยู่แล้ว

ส่วน monoid แปลเป็นไทยคร่าว ๆ ว่า "อะไรก็ได้ที่ถ้าเราเอามัน 2 ตัวที่เป็น type เดียวกันมารวมกัน มันจะรวมกันได้ และต้องได้ type เดิม" คำว่ารวมนี่ไม่ใช่การ + อย่างเดียวนะครับ ถ้า int monoid เราอาจจะบวกหรือคูณ ถ้า string หรือ list monoid เราก็เอามา concat ฯลฯ

จะสังเกตว่า monoid ไม่ใช่ context ของ "อะไรซักอย่าง" (kind: * -> *) แต่มันคือ ไอ้ตัว "อะไรซักอย่าง" เองเลยครับ (kind: *)