ทำความเข้าใจ Functor, Applicative Functor และ Monad ด้วย TypeScript
(evan-moon.github.io)เนื้อหา
- อธิบายกระบวนการไล่จากปัญหาสองอย่างที่แก้ไม่ได้ด้วย
mapของฟังก์เตอร์เพียงอย่างเดียว (ปัญหาที่ฟังก์ชันติดอยู่ในคอนเทนเนอร์ และปัญหาการซ้อนของบริบทเมื่อทำการ compose) ไปจนถึง applicative functor และ monad ด้วยโค้ด TypeScript - เริ่มจากภูมิหลังในปี 1988 ที่ Eugenio Moggi สร้างแบบจำลองโปรแกรมเป็น A → T(B) แทนที่จะเป็น A → B
- กล่าวถึงโครงสร้าง
flatMap = map + joinและกฎ 3 ข้อเพื่อใช้งานมันอย่างปลอดภัย (associativity, left identity, right identity) - อธิบายว่าทำไม monad จึงเป็น "วัตถุโมนอยด์ในหมวดเอนโดฟังก์เตอร์" โดยเทียบกับโมนอยด์ของการบวกจำนวนเต็ม
- กล่าวด้วยว่าเหตุใด Promise จึงทำงานแบบ monadic แต่ไม่ใช่ monad เชิงคณิตศาสตร์อย่างเคร่งครัด
ข้อจำกัดของฟังก์เตอร์: สิ่งที่ map ทำไม่ได้
- เมื่อใช้
mapกับฟังก์ชันแบบ curried ผลลัพธ์จะกลายเป็นMaybe<(b: number) => number>ทำให้ฟังก์ชันติดอยู่ในคอนเทนเนอร์mapรับได้เฉพาะฟังก์ชันที่อยู่นอกคอนเทนเนอร์ จึงไม่มีวิธีนำฟังก์ชันที่ติดอยู่ข้างในไปใช้กับค่าอื่น
- เมื่อ compose ฟังก์ชันสองตัวที่คืนค่าฟังก์เตอร์ จะเกิดการซ้อนของบริบทแบบ
Maybe<Maybe>- ยิ่งเพิ่มจำนวนขั้น ก็จะซ้อนแบบไม่สิ้นสุดเป็น
Maybe<Maybe<Maybe<...>>>
- ยิ่งเพิ่มจำนวนขั้น ก็จะซ้อนแบบไม่สิ้นสุดเป็น
แอปพลิเคทีฟฟังก์เตอร์: การนำฟังก์ชันในคอนเทนเนอร์ไปใช้
- ใช้โอเปอเรชัน
applyเพื่อเอาฟังก์ชันที่ติดอยู่ในคอนเทนเนอร์ไปใช้กับค่าจากอีกคอนเทนเนอร์หนึ่งได้apply: T<(A → B)> → T<A> → T<B>
- ใช้โอเปอเรชัน
pureเพื่อใส่ค่าบริสุทธิ์เข้าไปในคอนเทนเนอร์ - ข้อจำกัด: ต้องกำหนดล่วงหน้าว่าจะ compose คอนเทนเนอร์ใดบ้าง
- ไม่สามารถแสดงการพึ่งพาแบบลำดับเชิงพลวัตที่ตัดสินการคำนวณถัดไปจากผลลัพธ์ก่อนหน้าได้
Monad: การคิดค้นโอเปอเรชันสำหรับคลี่การซ้อน
- โอเปอเรชัน
joinจะคลี่คอนเทนเนอร์สองชั้นจากT<T<A>>ให้เหลือT<A>Array.prototype.flatของ JavaScript ทำหน้าที่เดียวกัน
- ในงานจริงมักใช้
flatMapที่รวมmap + joinเข้าด้วยกันflatMap: T<A> → (A → T<B>) → T<B>mapรับA → Bแต่flatMapรับA → T<B>จึงคงผลลัพธ์ไว้เพียงชั้นเดียว
กฎสามข้อของ flatMap
- กฎการประกอบเชื่อม: เมื่อคลี่
T(T(T(A)))ไม่ว่าจะคลี่จากชั้นในก่อน
หรือจากชั้นนอกก่อน ผลลัพธ์ต้องเหมือนกันm.flatMap(f).flatMap(g) === m.flatMap(x => f(x).flatMap(g))
- กฎเอกลักษณ์ซ้าย: ถ้าใส่ค่าด้วย
pureแล้วflatMapทันที ต้องได้ผลเหมือนเรียกใช้ฟังก์ชันนั้นโดยตรงpure(a).flatMap(f) === f(a)
- กฎเอกลักษณ์ขวา: ถ้าส่ง
pureให้flatMapต้องได้คอนเทนเนอร์เดิมกลับมาm.flatMap(pure) === m
การแยกความหมายของ "วัตถุโมนอยด์ในหมวดเอนโดฟังก์เตอร์"
- ฟังก์เตอร์ในโปรแกรมมิงมุ่งจากโลกของชนิดข้อมูลไปยังโลกของชนิดข้อมูล จึงเป็นเอนโดฟังก์เตอร์
- สามารถสร้างหมวดเอนโดฟังก์เตอร์ที่ใช้ตัวเอนโดฟังก์เตอร์เองเป็นวัตถุได้
- เมื่อนำไปเทียบกับเงื่อนไขของโมนอยด์ (การดำเนินการทวิภาค + กฎการประกอบเชื่อม + เอกลักษณ์) จะได้ว่า:
- การดำเนินการทวิภาค =
join - เอกลักษณ์ =
pure - โครงสร้างนี้สอดคล้องตรงกันพอดีกับโมนอยด์ของการบวกจำนวนเต็ม
- การดำเนินการทวิภาค =
เหตุผลที่ Promise ไม่ใช่ monad
thenจัดการแบบผสมระหว่างmapและflatMapตามค่าที่คืนกลับ- สถานะแบบ
Promise<Promise>ไม่ถูกอนุญาตในรันไทม์ และจะถูกรวมเป็นชั้นเดียวทันที - แม้จะสะดวกในทางปฏิบัติ แต่ก็ไม่สอดคล้องกับกฎ monad ทางคณิตศาสตร์
2 ความคิดเห็น
ช่วยพูดถึง Comonad ด้วยนะครับ!
อืม...ขอคิดดูก่อนครับ 5555