DEV Community

Weerasak Chongnguluam
Weerasak Chongnguluam

Posted on

เทคนิคที่ Haskell ใช้เพื่อทำให้ภาษาเป็น purely functional

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"
Enter fullscreen mode Exit fullscreen mode

แน่นอนเวลาเรารันโปรแกรมนี้เราจะได้ "Hello, World" ออกมาเช่นกัน

ลองอีกตัวอย่างแบบรับชื่อมาแสดงแทน World

main :: IO ()
main = do
  putStr "Name: "
  name <- getLine
  putStrLn ("Hello, " <> name)
Enter fullscreen mode Exit fullscreen mode

เมื่อเรารันก็จะได้แบบนี้

> runghc Main.hs
Name: Por
Hello, Por
Enter fullscreen mode Exit fullscreen mode

ตัวอย่างนี้ทั้งแสดงผลและรอรับอินพุตเลยก็ทำได้เช่นกัน

แล้วอะไรยังทำให้ Haskell เป็น purely function อยู่ เรามาดูกันที่ฟังก์ชัน putStrLn กันก่อน

ถ้าเราดู type ของ putStrLn เราจะเห็นว่ามันมี type แบบนี้

putStrLn :: String -> IO ()
Enter fullscreen mode Exit fullscreen mode

นั่นคือรับ 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 #))
Enter fullscreen mode Exit fullscreen mode

จะเห็นว่า 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 ()
Enter fullscreen mode Exit fullscreen mode

จะเห็นว่า 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)
Enter fullscreen mode Exit fullscreen mode

แต่ละบรรทัดใน 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
Enter fullscreen mode Exit fullscreen mode

ถ้าเราแปลงโค้ดกลับมาจะเป็นแบบนี้

main :: IO
main = (putStr "Name: ") >> (getLine >>= (\name -> putStrLn ("Hello, " <> name)))
Enter fullscreen mode Exit fullscreen mode

จะเห็นว่าทั้ง >>= และ >> นั้นทำหน้าแค่แค่เอา 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
Enter fullscreen mode Exit fullscreen mode

จะเห็นว่ามันรับ 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 คลิ๊กที่รูปด้านล่างนี้ได้เลยครับ

Buy Me A Coffee

ส่วนท่านใดไม่สะดวกใช้บัตรเครดิต หรือ Paypal สามารถสนับสนุนผมได้ผ่านทาง PromptPay โดยดู QR Code ได้จากโพสต์ที่พินเอาไว้ได้ที่ Page DevDose ครับ https://web.facebook.com/devdoseth

Top comments (0)