Haskell มี side effect ให้ใช้แน่ๆไม่งั้นเขียนโปรแกรมใช้งานจริงๆไม่ได้ แต่ Haskell ก็ยังเคลมได้ว่าเป็น purely functional programming language ได้อยู่ดี เพราะอะไรนั้นมาดูกัน
Pure function
มาที่นิยามของ pure function ก่อน ง่ายๆคือ function ที่ไม่มี side effect ใดๆ เวลา apply ด้วย parameter เดิม ก็จะได้ output ออกมาเป็นค่าเดิมเสมอ เช่นเรียก sum(10, 20)
ก็ได้ 30
เสมอ
Side Effect
ทีนี้ function ที่มี side effect ก็คือเวลาเราเรียก แล้วมันไม่ใช่แค่ได้ output แต่มันกระทบกับระบบอื่นๆด้วยเช่น puts "Hello"
ใน Ruby มันจะตอบกับมาเป็น nil
และส่ง "Hello"
ไปแสดงที่ terminal ด้วย
Side Effect ใน Haskell
ทีนี้มาดูใน Haskell เช่นโปรแกรม Hello World ง่ายๆนั้นเขียนได้แบบนี้
main :: IO ()
main = do
putStrLn "Hello, World"
แน่นอนเวลาเรารันโปรแกรมนี้เราจะได้ "Hello, World" ออกมาเช่นกัน
ลองอีกตัวอย่างแบบรับชื่อมาแสดงแทน World
main :: IO ()
main = do
putStr "Name: "
name <- getLine
putStrLn ("Hello, " <> name)
เมื่อเรารันก็จะได้แบบนี้
> runghc Main.hs
Name: Por
Hello, Por
ตัวอย่างนี้ทั้งแสดงผลและรอรับอินพุตเลยก็ทำได้เช่นกัน
แล้วอะไรยังทำให้ Haskell เป็น purely function อยู่ เรามาดูกันที่ฟังก์ชัน putStrLn
กันก่อน
ถ้าเราดู type ของ putStrLn
เราจะเห็นว่ามันมี type แบบนี้
putStrLn :: String -> IO ()
นั่นคือรับ String เข้ามาแล้วได้ IO () กลับออกมา
ความฉลาดของคนออกแบบ IO อยู่ตรงนี้คือ IO เป็น type ที่มันห่อฟังก์ชันเอาไว้ ถ้าเราใช้ :i IO
ใน ghci เราจะเห็นการประกาศ type มันแบบนี้
newtype IO a
= GHC.Types.IO (GHC.Prim.State# GHC.Prim.RealWorld
-> (# GHC.Prim.State# GHC.Prim.RealWorld, a #))
จะเห็นว่า IO
wrap ค่าฟังก์ชันที่รับ GHC.Prim.State# GHC.Prim.RealWorld
แล้ว return (# GHC.Prim.State# GHC.Prim.RealWorld, a #)
นั่นคือเวลาเราเรียก putStrLn "Hello, World"
มันไม่ใช่การเรียกเพื่อให้เกิด effect แต่มันเป็นการเรียกเพื่อให้เกิดการสร้าง function เอาไว้ซึ่งฟังก์ชันนี้จะมี side effect เมื่อถูกเรียกนั่นเอง
คือใน Haskell เราจะเห็น type IO a
เต็มไปหมด มันเป็น type ที่ห่อฟังก์ชันที่มี side effect เอาไว้ และ ถ้ามีการเรียก มันจะเกิด side effect และได้ค่าของ type a
เป็น output ด้วยนั่นเอง
แล้วใครมันเรียก side effect function ที่ซ่อนไว้ใน IO
เนี่ยแหละเป็นเทคนิคที่ฉลาดมาก คือ Haskell มี IO ที่ห่อ side effect function แต่ไม่มีวิธีเรียกใช้ side effect นั้นโดยตรง ทำให้เคลมได้ว่า function ใน Haskell นั้นไม่มี side effect
เราเรียก putStrLn "Hello, World"
ในโค้ดเรากี่รอบก็ได้ แต่ตอนเราเรียกสิ่งที่ได้ก็คือแค่ IO ()
มันยังไม่ได้เอา "Hello, World"
ไปแสดงผลทันที
แล้วใครเรียก?
กลับไปดู type ของ main function อีกที
main :: IO ()
จะเห็นว่า main ก็มี type เป็น IO
นั่นคือจริงๆแล้ว main เป็นแค่ค่าที่ห่อฟังก์ชันที่มี side effect ไว้เช่นกันเมื่อโดนเรียก
ทีนี้คนที่เรียก main ก็คื runtime ของ Haskell นั่นเอง ที่เรียกตอนเราเรียกโปรแกรมให้ทำงาน
เป็นเทคนิคที่ฉลาดมากๆ เพราะทำแบบนี้ ทำให้โค้ด Haskell นั้นยังคงถูกต้องการหลัก pure function แต่ก็สามารถเขียนโปรแกรมให้มี side effect ได้
ร้อยเรียง side effect ด้วย IO Monad
เรารู้ไปแล้วว่า IO จริงๆแล้วก็คือ type ที่ห่อฟังก์ชันที่มี side effect เอาไว้ ทีนี้สิ่งที่ Haskell เตรียมมาด้วยคือ implements Monad typeclass ให้กับ IO ด้วย เพื่อให้เราสามารถ bind side effect หลายอันเข้าด้วยกันให้เกิดเป็น IO อันใหม่ที่ห่อ side effect อันใหม่เอาไว้
ถ้าเราย้อนกลับไปดูโค้ดอีกรอบ
main :: IO ()
main = do
putStr "Name: "
name <- getLine
putStrLn ("Hello, " <> name)
แต่ละบรรทัดใน do
notation นั้นจริงๆแล้วจะถูกแปลงไปเป็นการเรียกฟังก์ชัน (>>=) :: m a -> (a -> m b) -> m b
หรือ (>>) :: m a -> m b -> m b
ของ Monad typeclass
พอเป็น IO Monad สองฟังก์ชันนั้นก็จะเป็น type แบบนี้
(>>=) :: IO a -> (a -> IO b) -> IO b
(>>) :: IO a -> IO b -> IO b
ถ้าเราแปลงโค้ดกลับมาจะเป็นแบบนี้
main :: IO
main = (putStr "Name: ") >> (getLine >>= (\name -> putStrLn ("Hello, " <> name)))
จะเห็นว่าทั้ง >>=
และ >>
นั้นทำหน้าแค่แค่เอา IO มาร้อยเรียงกันให้เป็น IO อันใหม่ที่ห่อ side effect function เอาไว้เมื่อรันแล้วถึงจะเกิด effect ต่างๆ แต่เราไม่มีทางเรียกให้เกิด side effect ตรงๆ ในโค้ดเราได้นั่นเอง ทำได้แค่สร้างให้เป็น main :: IO () อันใหม่ก้อนนึงที่จะถูกเรียกเมื่อรันโปรแกรม (จริงๆมี :P)
Dark side unsafe
เราเห็นไปแล้วว่าโดยปกติไม่มีทางที่เราจะรัน side effect ที่อยู่ใน IO ได้ แต่อย่างไรก็ตามยังมีฟังก์ชันที่รันได้ ซึ่งถือว่าแหกกฎของ pure function ของ Haskell ดังนั้นมันเลยถูกมองว่าเป็น unsafe function เช่นฟังก์ชัน unsafePerformIO
ใน System.IO.Unsafe
module ที่มี type แบบนี้
unsafePerformIO :: IO a -> a
จะเห็นว่ามันรับ IO a
แล้วได้ค่ากลับมาเป็น a
ได้ สิ่งที่มันทำคือรัน side effect ที่ห่อใน IO นั้นพร้อมกับเอาผลลัพธ์ a กลับมานั่นเอง
สรุป
Haskell นั้นเป็น purely functional programming language โดยที่ฟังก์ชันของ Haskell ทั้งหมด ยกเว้นพวก unsafe เวลาเรียกแล้วจะได้ค่าเดิมเสมอ ไม่มี side effect ใดๆ
แต่ side effect นั้นจะถูกจัดการด้วย IO ที่จะทำหน้าที่แค่ห่อ side effect ฟังก์ชันไว้ข้างใน ดังนั้นเราจึงจัดการ side effect ที่ห่อใน IO ได้คือร้อยเรียงมันให้เป็น IO ก้อนใหม่ที่เมื่อถูกเรียกถึงจะเกิด side effect จริงๆ
ทำให้การจัดการ side effect ถูกจัดการได้โดย pure function ได้เหมือนค่าอื่นๆปกตินั่นเอง
ขอฝาก Buy Me a Coffee
สำหรับท่านใดที่อ่านแล้วชอบโพสต์ต่างๆของผมที่นี่ ต้องการสนับสนุนค่ากาแฟเล็กๆน้อยๆ สามารถสนับสนุนผมได้ผ่านทาง Buy Me a Coffee คลิ๊กที่รูปด้านล่างนี้ได้เลยครับ
ส่วนท่านใดไม่สะดวกใช้บัตรเครดิต หรือ Paypal สามารถสนับสนุนผมได้ผ่านทาง PromptPay โดยดู QR Code ได้จากโพสต์ที่พินเอาไว้ได้ที่ Page DevDose ครับ https://web.facebook.com/devdoseth
Top comments (0)