เทคนิคที่ Haskell ใช้เพื่อทำให้ภาษาเป็น purely functional
Tags: haskell
Published: 2021-05-13 01:02:33

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 ง่ายๆนั้นเขียนได้แบบนี้

```haskell
main :: IO ()
main = do
  putStrLn "Hello, World"
```

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

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

```haskell
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 มันแบบนี้

```haskell
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 อีกที

```haskell
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 อันใหม่เอาไว้

ถ้าเราย้อนกลับไปดูโค้ดอีกรอบ

```haskell
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 แบบนี้

```haskell
(>>=) :: IO a -> (a -> IO b) -> IO b

(>>) :: IO a -> IO b -> IO b
```

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

```haskell
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 คลิ๊กที่รูปด้านล่างนี้ได้เลยครับ

<a href="https://www.buymeacoffee.com/weerasak" target="_blank"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me A Coffee" style="height: 60px !important;width: 217px !important;" ></a>

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