雖然 Haskell 是一個純粹的 FP 語言,這意味著它的函數更接近數學意義上的「函數」,也就是我們所說的 pure function 。它的特點是其輸出只依賴於其輸入,不管何時何地執行都會產生相同的結果。
然而,在實際開發中,我們通常需要處理一些與外部環境互動的情況,像是讀寫文件、發送網絡請求等。這些操作通常具有副作用,而 Haskell 提供了一種特殊的類型 IO
來表示這種可能具有副作用的計算。
在 Haskell 中,我們可以使用 IO
類型來表示這些 I/O 操作,並且使用 Haskell 的型別系統和 monadic (就是具有 monad 特性,也可以就是在某個 context 下)結構,我們可以將純函數與 I/O 操作分離,以確保程式的可預測性和穩定性。
:t print -- print :: Show a => a -> IO ()
:t getLine -- getLine :: IO String
這邊會看到 print
是接受一個 Show a
最後回傳 IO action ,而 getLine
就是會回傳一個 String
的 IO action
基本上 IO
就是一種 Monad
,那它為什麼需要成為一個 Monad
?這是為了讓我們可以把side effect 切分出來,讓有 side effect 的操作是在另外一個 context 下進行。
我們先來寫一個小程式來理解這一切
import Text.Read (readMaybe)
main::IO()
main = do
print "Enter a number"
x <- readMaybe <$> getLine
print "Enter another number"
y <- readMaybe <$> getLine
case (+) <$> x <*> y of
Nothing -> print "Invalid input"
Just z -> print z
這邊有幾個比較重要的東西是 ←
及 do
,首先 getLine
會回傳一個 IO String
但,然後使用 ←
綁定 x
,第一次看到可能會有一點疑惑為什麼不是使用 =
,而是使用 ←
。因為 getLine
本身是一個 IO action 或者該說 monadic value ,所以我們直接 =
其實也就是讓一個變數變成 getLine
而不是將讀取到的字串從 context 中取出來。
所以必須使用了 ←
來達成這件事情, ←
真正的意義是將 monadic 操作綁定到一個變數的 operator。
那 do
呢?可以理解它幫我們逐層使用了 >>=
,也就是說每一行都是一個 monadic value,,我們先看一下沒有 do
的程式碼大概會長怎麼樣
main' = print "Enter a number" >> getLine >>= \x ->
print "Enter another number" >> getLine >>= \y ->
print $ "The sum is: " ++ show (read x + read y :: Int)
我們就得處理各個 monadic 操作之間的串連,像是 print
後使用 >>
來進行下一個 monadic 操作,然後 getLine
用 >>=
傳給一個 lamda。
回憶一下
>>= :: m a -> (a -> mb) → mb
之後再用 →
將 lamda 往之後的操作 bind 。
所以再看一次原本的程式碼
import Text.Read (readMaybe)
main::IO()
main = do
print "Enter a number"
x <- readMaybe <$> getLine
print "Enter another number"
y <- readMaybe <$> getLine
case (+) <$> x <*> y of
Nothing -> print "Invalid input"
Just z -> print z
如果有讀者之前就在想為什麼會需要標註 main::IO()
經過這過篇章相信大家應該都有答案了,因為在 do
語法的最後會是回傳一個 monadic value 就像 >≥
一樣。
那知道這些事情後我們還能做這些事情
foo :: Num a => a -> a -> Maybe String
foo a b= do
x <- Just a
y <- Just b
Just (show x ++ y)
←
及 do
不一定要在 main
中使用,也不一定用於 IO
只要是 monadic的操作或值我們都能用它們來讓我們的程式碼更好讀。
至此應該開始能理解 monad 到底是幹嘛的了,簡單來說就是可以把一些需要按照順序執行或者有副作用的操作包裝近一個 context ,讓這些不夠 pure 的行為可以跟其他程式切開來,藉此讓其他的程式繼續維持是沒有 side effect 的狀況。
小小總結是
Functor
提供我們 map 一個 context 的行為Applicative
提供我們將 function apply 到 context 的行為Monad
則是提供了處理副作用及控制順序的行為,也就是說我們基本上不必擔心 context 是什麼只要關注 context 裡面的值就好,藉由這種行為我們可以簡單的分離有 side effect 的操作。明天我們將繼續探討其他 Monad
今天的程式碼:
https://github.com/toddLiao469469/30days-for-haskell