Functional Programming in Python Part 4: Monad !!!!!
VDO อยู่ตรงนี้ -> Youtube
Slide อยู่ตรงนี้ -> PowerPoint
Code ตัวอย่างอยู่ตรงนี้ -> Github
ตอนที่สาม เรามาถึงจุดที่ เราไม่สามารถจัดการ Error ด้วย toolz ในรูปแบบที่เราต้องการได้ก็เลยเป็นที่มาให้เรามองหา เครื่องมือมาช่วยพวกเราและสิ่งที่ทำให้เราได้มาซึ่งสิ่งที่เราอยากได้คือ Monad
Monad — Maybe Monad
มันคืออะไรนะจริงๆแล้วถ้าจะเอา Theory มาเล่าให้ฟังน่าจะต้องอ้อมไปไกลมากตั้งแต่ Applicative, Functor, Monoid แล้วก็พวก properties ของมันเช่น Unit, Bind, Associativity คงจะเขียนได้อีกสองเดือน แต่ถ้าจะให้เอาแบบบ้านๆหรือแบบรูฟๆ ก็ขอเล่าว่า Monad คือ pattern ของ functional programming ที่เราเอาไว้จัดการการทำงานของโปรแกรมที่อาจมีผลของการทำงานที่เกินกว่าที่เราคาดหวังหรือเราเรียกสิ่งเหล่านี้ว่า side effect (Exception Handing, IO Operation, …) ตัวอย่างเช่น
def read_csv_file(file_path):
try:
with open(file_path, 'r') as csvfile:
return [row for row in csv.reader(csvfile)]
except FileNotFoundError as e:
raise FileNotFoundError(f"Error reading file: {e}")
เราเห็นเลยว่า function นี้มี side effect เพราะถ้าไม่เจอไฟล์มันจะทำการ raise สิ่งที่เรียกว่า FileNotFoundError และที่ main program เราจัดการจับมันด้วย try-except และแน่นอนว่านี่ ไม่ใช่การจัดการแบบ functional เพราะ exception (ย้ำ!!!!!!!! ว่าเขียนแบบนี้เป็น side effect) และเราจะเห็นว่า ของที่ return ออกมาจาก read_csv_file มันเป็นของสองประเภทที่ต่างกันโดยสิ้นเชิง
ถ้าสำเร็จจะได้ data ออกมา แต่ถ้าไม่สำเร็จจะได้ exception
แล้ว Monad คืออะไรเรามาเริ่มจาก Monad ที่บ้านๆมากที่สุดอันหนึ่งก่อนชื่อ Maybe Monadเป็น Monad ที่สร้างขึ้นมาเพื่อจัดการกับโปรแกรมที่เราคาดว่าจะเกิดผลสองอย่างคือถ้ามันทำงานถูกเราจะได้ผลออกมาเป็นอะไรบางอย่าง (Some) แต่สิ่งที่สำคัญคือถ้ามันไปตกตอนที่ผิดพลาดเราจะไม่สนใจผลที่เกิดขึ้น (Nothing) แต่ถ้าเราสนใจเดี๋ยวเราไปเที่ยวแถวๆ Maybe Either ตอนนี้เอาแค่นี้ก่อน ถึงตรงนี้เรามาจะมา Model สิ่งที่เราเขียนไปออกมาเป็นภาพเราจะได้แบบนี้
ถ้าสำเร็จจะได้ Maybe[Some] ออกมา แต่ถ้าไม่สำเร็จจะได้ Maybe[Nothing]
ถ้าทั้งระบบของเรามี Maybe และตัวมันเองแยกออกมาได้สองประเภทคือ Some และ Nothing มีเท่านี้จริงและสิ่งที่เราต้องการคือฟังก์ชั่นของเราไม่ว่ามันจะรับ input อะไรเข้ามามันจะ return Maybe เสมอ ถ้ามันทำงานได้อย่างที่เราอยากได้เราจะเอาผลที่ได้จะเป็น Maybe-Some แต่ถ้ามันทำไม่ได้เราจะเอาข้อความผิดพลาดผลที่ได้จะเป็น Maybe-Nothing
ฟังก์ชั่นนี้จะไม่มี side effect !!!!!!! เพราะอะไร เพราะมันจะ return Maybe เสมอ
แถวนี้เราเราเรียกมันว่า Unit เป็นคุณสมบัติอีกอันของ Monadและพอเราทำแบบนี้ได้เราก็จะพบว่าเราสามารถเอาฟังก์ชั่นที่ return Maybe นี้มาเรียงต่อกันได้เลยแบบนี้
แต่ว่าปัญหามันจะเกิดตอนที่มันโยนของต่อไป เพราะว่า function ของเรา ขาเข้าไม่ได้รับ Maybe อย่างเดียวเราต้องรับ function ต่อไปเพื่อเอามา apply ด้วยเป็นทอดๆไป เราจะทำยังไง ?????? เราเลยต้องมีตัวละครอีกตัวที่เรียกว่า bind หรือ >>= ที่ทำหน้าที่เอา Maybe ที่ได้จาก function ก่อนหน้ามาใส่ใน function ต่อไปอีกฟังก์ชั่นนั่นเอง คุ้นๆนะเหมือนเราทำ function composition ง่ายๆจากตอนที่แล้วด้วย higher-order function
เราเรียกแถวนี้ว่า bind หรือ flatMap เป็นคุณสมบัติอีกอันของ Monad
ก่อนที่จะตาลอยไปกว่านี้เราลองมาดูตัวอย่างก่อน
class Maybe:
def is_some(self):
return self.__class__ == Some
class Some(Maybe):
def __init__(self, value):
self.value = value
def bind(self, func):
return func(self.value)
def __str__(self):
return f"Some({self.value})"
class Nothing(Maybe):
def bind(self, func):
return self
def __str__(self):
return f"Nothing"
def divide(a, b):
if b == 0:
return Nothing()
else:
return Some(a / b)
result = (
Some(10)
.bind(lambda y: divide(y, 2))
.bind(lambda y: divide(y, 0)) # This in Nothing
)
if result.is_some:
print(f"Result is Right: {result}")
else:
print(f"Result is {result}")
สิ่งที่เราเห็นชัดๆคือ bind function ของ Some จะทำหน้าที่ call function ที่เรารับมาเป็น lambda (เพราะเราอยาก chain binary function) แล้ว return Maybe-Some ออกไปแต่ในทางกลับกันถ้าเป็น bind function ของ Nothing มันจะ doing nothing หรือเราเรียกว่าปล่อยจอย โยนละ ไม่ทำอะไรต่อ สิ่งนี้เราเรียกมันว่า Maybe Monad !!!!!! และมันเป็นหนึ่งใน Monad หลายประเภทไม่ว่าจะเป็น IO Monad, Reader Monad, State Monad, Identity Monad, … Monad ซึ่งแต่ละตัวก็จะมีหน้าที่รับผิดชอบแตกต่างกันไป
ความสนุกของ Monad คือเราสามารถสลับของได้แบบตัวอย่างเช่นเอาเปลี่ยนตำแหน่งผลต้องได้เหมือนเดิม สิ่งนี้เราเรียกว่า Associativity อันนี้จะมึนๆ ข้ามไปก่อนได้เดี๋ยวค่อยกลับมาอ่านครับ
ถ้า
m
เป็น monadm >>= (\x -> f x >>= g)
ควรเท่ากับ(m >>= f) >>= g
.
# Expression: m >>= (\x -> f x >>= g)
result_left = (
Some(10)
.bind(lambda y: divide(y, 2).bind(lambda z: divide(z, 0)))
)
# Expression: (m >>= f) >>= g
result_right = (
Some(10)
.bind(lambda y: divide(y, 2))
.bind(lambda z: divide(z, 0))
)
เพิ่มเติมให้นิดหน่อยเจ้า Maybe Monad บางสำนักจะเขียนภาพออกมาเป็นรางรถไฟและเรียกฟังก์ชั่นแต่ละอันว่าเป็นแยกของรางและเอามันมาต่อกัน ด้วยวิธีคิดแบบนี้เขาเหล่านั้นเรียกมันว่า Railway Oriented Programming (ROP) ที่เป็นแบบ bypass รายละเอียดปล่อยไว้ก่อนเดี๋ยวมาเล่าให้ฟัง
วันนี้เล่าไม่หมดนะครับเอาแค่ Maybe ไปก่อน อย่างไรก็ตามเราเขียน Monad เองเพียงเพราะว่าเราอยากเข้าใจกระบวนการคิดพื้นฐานของมันแต่ตอนทำงานจริงเราก็ไม่ทำท่านี้นะ เราไปใช้ library กันดีกว่าและตัวที่เราจะใช้ชื่อ PyMonad
PyMonad — Monad Library For Python
ชื่อมันก็บอกละว่ามัน implement Monad ด้วย Python และตัวมันเองก็มี Monad ให้ใช้หลายตัวมากแต่ตัวที่เราจะใช้ก่อนเลยคือ Either Monad สิ่งที่ Either Monad ต่างจาก Maybe Monad คือเราให้ความสำคัญกับส่วนผิดพลาดเพราะเราต้องการรายละเอียดเพื่อเอาไปทำงานเพิ่มเติมไม่ได้ปล่อยผ่านไปดังนั้นการจัดการผลที่ได้ออกมาจากการทำงานจะต่างจาก Maybe ที่จะได้ Some หรือ None เจ้า Either Monad จะได้เป็น Either-Right (ทำงานได้) หรือ Either-Left (ทำงานไม่ได้)ไม่วาดรูปแล้วนะครับ เหนื่อยดู code กัน
from pymonad.either import Left, Right
def divide(a, b):
return (
Right(a/b)
if b != 0
else Left("Error: Division by zero")
)
# Example usage
result = divide(10, 2) # Result: Right(5.0)
result = divide(10, 0) # Result: Left('Error: Division by zero')
print(result)
แล้วถ้าเราอยากทำ composition ต้องทำไงเราไปท่าเดิมของเรากันคือขี้เกียจเขียน lambda เราเลยบอกว่าให้ divide เป็น curry นะและ PyMonad ก็ทำ curry มาให้แล้วเราเลยเอาใช้พร้อม bind function ที่เขียนว่า then ได้เลย
from pymonad.either import Left, Right
from pymonad.tools import curry
@curry(2)
def divide(a, b):
return (
Right(a/b)
if b != 0
else Left("Error: Division by zero")
)
result = Right(10).then(divide(2)).then(divide(5))
print(result)
ตอนนี้เราเห็นภาพละว่า Monad เป็นไงอย่างน้อยก็สองตัว Maybe กับ Either คราวนี้เรามาดูกันว่าโค้ดของเราเมื่อจับ PyMonad ใส่เข้าไปแล้วออกมาหน้าตาเป็นไง
import csv
import os
from pymonad.tools import curry
from pymonad.either import Left, Right
def read_csv_file(file_path):
if os.path.isfile(file_path):
with open(file_path, 'r') as csvfile:
return Right([row for row in csv.reader(csvfile)])
else:
return Left("Error: File not found")
# Something in between
# ...
# -------------------------------
def calculate_average(column_values):
if len(column_values) > 0:
return Right(sum(column_values) / len(column_values))
else:
return Left("Error: Division by zero")
# Detial Above
result = (
read_csv_file(csv_file_path)
.then (extract_score_column)
.then (remove_header)
.then (convert_score_to_float)
.then (calculate_average)
)
if result.is_right():
print(f"An average score is {result}")
else:
print(f"Error processing data: {result}")
สวยงามตามท้องเรื่อง ที่เราอยากได ทุกฟังก์ชั่น return Either-Right หรือ Either-Left แล้วเรามาดูผลที่ได้ตอนจบครั้งเดียวไม่มี try-excection raise แล้ว ที่สำคัญที่สุดคือเราได้เห็นความเปลี่ยนแปลงของ code เราตั้งแต่เริ่มต้นและค่อยๆเปลี่ยนมาจนถึงตรงนี้ จริงๆแล้วโค้ดชุดนี้เองก็ไม่ได้ดีไปเสียทุกด้าน สิ่งเดียวที่มันทำได้ดีคือมันสอนให้เราตั้งคำถามว่าถ้าเราอยากแก้ปัญหาด้วยแนวคิดพื้นฐานของ functional programming เราจะไปได้อย่างไรบ้างและอย่างน้อยมันก็ทำให้เราเห็นว่า ความรู้พื้นฐานนั้นสำคัญ ถ้าเรามีสิ่งนี้เราสามารถนำมันไปประยุกต์ใช้กับเครื่องมืออะไรก็ได้และผลที่ได้ออกมาคือการเรียนรู้
ท้ายสุด ผมเองเขียน python ไม่คล่องนะครับเขียนจริงจังเมื่อ สองสัปดาห์ก่อนจะไปพูดที่งาน PyCon Thailand 2023 ครับดังนั้นโค้ดที่ผมเขียนนี้ชาว python เขาอาจจะไม่เขียนกันแบบนี้เลยผมต้องขออภัยไว้ด้วยครับ ถ้ามีอะไรผิดพลาดสามารถ feedback ผมได้ด้วยการพาผมไปเลี้ยงข้าว ไม่สิ ส่ง message หาผมได้ทาง discord (rufimon) ได้เลยนะครับผมพร้อมเรียนรู้จากทุกท่านครับ
Credit
สุดท้ายผมอยากให้ credit กับคนที่ทำให้ผมเห็นโลกของ functional programming ที่เปลี่ยนไป ไม่ม่อะไรจะพูดมากไปกว่าคำว่า
ขอบคุณอาจารย์เดฟมากครับ
Spoil !!!!!!! Monad เข้ากับภาษานี้ได้ดีมาก
package main
import (
"fmt"
)
// Maybe represents a computation that may or may not produce a value.
type Maybe struct {
hasValue bool
value interface{}
}
// Something in between ...
func main() {
// Use the Maybe monad
result := Just(3).Map(addOne).Map(square).Map(double)
// Perform an action if the Maybe is of type Just
if result.IfJust() {
fmt.Println("Value inside Maybe:", result.value)
} else {
fmt.Println("No valid result for IfJust.")
}
}