Haskell Tutorial(19)Data.Set 與 Data.Map 模組 << 前情
純函數式的世界泡久了,現在換換口味,暫時探出頭來看看不純綷的世界好了,實際上,你還是得與真實世界溝通,你得接受輸入,在結果運算出來之後,再輸出到真實世界之中,這表示,你總得有個地方,某些函式每次呼叫的結果並不一定會是相同的,你接受使用者的輸入不會總是相同的,你要顯示的資料會改變終端機的狀態。
從 main 開始
重新來看看第一個〈Haskell Tutorial(1)哈囉!世界!〉中第一個程式:
main = putStrLn "哈囉!世界!"
putStrLn 函式會有副作用,它會改變真實世界的狀態,也就是終端機的顯示狀態,每執行一次 putStrLn ,終端機的顯示就會多一行指定的字串顯示。
在 Haskell 中,每個函式都要有傳回值,對於輸出資料至終端機的 putStrLn ,能有什麼樣的傳回值?
String -> IO () 表示 putStrLn 接受 String ,並傳回一個 IO () ,IO 是代表輸入輸出這類動作的型態,當中可以包括想與真實世界做溝通的值,對於輸出資料至終端機,實際上不需要什麼傳回值,因此傳回的 IO 包括了一個空的 Tuple,因此顯示為 IO () 。
實際上,在 putStrLn 傳回 IO () 之後,還不會對終端機做任何改變,任何 IO 在成為 main 執行後的傳回值前,都不會有任何的作用,例如:
main = do
let io = putStrLn "Hello, world!"
putStrLn "哈囉!世界!"
在上面這個程式中,putStrLn "Hello, world!" 實際上只是指定給 io 名稱,並沒有被 do 串起來,成為一串 IO 中的一部份,所以這個程式實際上只會顯示 "哈囉!世界!" ,而不會顯示 "Hello, World!" 。
實際上,main 本身也是個函式,它也會有型態,願意的話,你可以如下幫它加上型態,只是慣例上不會加上而已,例如執行 putStrLn 的結果是 IO () ,因此可以這麼定義 main 的型態:
main :: IO ()
main = putStrLn "哈囉!世界!"
綁定 IO 中的值
來看看取得使用者輸入的情況,例如下面這個程式:
main = do
name <- getLine
putStrLn ("哈囉!" ++ name ++ "!")
getLine 會取得使用者的輸入,它的型態是什麼呢?
前面說過,IO 是代表輸入輸出這類動作的型態,當中可以包括想與真實世界做溝通的值,getLine 的傳回值是 IO String ,表示這個 IO 中包括了個字串,而這個字串來自使用者的輸入。
你不能使用 name = getLine ,這只是表示 name 的值是 IO String ,而不是 IO 中的 String ,想取得 IO 中的值,必須使用 <- ,這就是為什麼寫成 name <- getLine 的原因,以下故意先取得 IO String ,再從 IO String 中取得 String ,做為一個比較:
main = do
let io = getLine
name <- io
putStrLn ("哈囉!" ++ name ++ "!")
當然,直接寫成 name <- getLine 就可以了,在 Haskell 的慣例中,稱這是將 getLine 傳回的 IO String 中之字串值「綁定(bind)」給 name 。
實際上,你也可以撰寫 variable <- putStrLn "哈囉!世界!" ,只是沒什麼意義,因為 putStrLn 的傳回值是 IO () ,最後你只是將 IO 中的 () 綁定給 variable 而已,綁定一個空 Tuple 不能做什麼,因此不會這麼做。
這也表示,對於傳回 IO 的函式,你不一定要使用 <- ,例如以下,只是將 getLine 取得的結果丟掉而已:
main = do
getLine
getLine
putStrLn "哈囉!世界!"
先簡單談談 do
do 的最後一個動作不能使用 <- 做綁定,想知道為什麼得瞭解什麼是 Monad,IO 是個 Monad,具體來說,是具有 Monad Typeclass 的行為,do 實際上是將一連串的 Monad 連接在一起,這之後才會談到,暫且先記得這個限制就好了。
簡單來說,可以先將 do 理解為,可將一連串會傳回 IO 的函式串接在一起,成為一個更大的 IO ,具體來說,do 定義了一個函式呼叫(或說是運算式),而這個被呼叫的函式中包括了數個會傳回 IO 的函式,而 do 定義的函式在呼叫過後傳回的結果型態,取決於其包括的函式中,最後一個函式傳回的型態。
例如,上頭的 main ,型態會是 main :: IO () ,因為 do 中最後一個函式為 putStrLn ,其傳回型態是 IO () ,如果改為以下:
main = do
getLine
putStrLn "哈囉!世界!"
getLine
那麼 main 的型態就會是 main :: IO String ,因為 do 中最後一個函式是 getLine ,其傳回型態是 IO String 。
純粹跟非純粹
暫時可以這麼說,函式中若包括了會產生 IO 的函式,它就變成也得傳回 IO ,為了達到這個目的,比較簡單的方法是將其他與該函式相關聯的程式碼,串聯起來成為一個會傳回 IO 的運算,就目前為止,你知道的方式就是用 do ,例如,以下這個會編譯錯誤:
doubleIt input =
let number = read input::Int
output = number * 2
putStrLn $ "Double your " ++ input
putStrLn $ show output
main = do
input <- getLine
doubleIt input
這是因為 doubleIt 函式中,包括了會傳回 IO () 的 putStrLn 函式,想解決這個問題,要嘛就是讓 doubleIt 成為非純綷、具副作用,也就是會產生 IO 的函式,像是使用 do 將整個串起來:
doubleIt input = do
let number = read input::Int
output = number * 2
putStrLn $ "Double your " ++ input
putStrLn $ show output
main = do
input <- getLine
doubleIt input
要嘛就是讓 doubleIt 成為純綷、無副作用的函式:
doubleIt input =
let number = read input::Int
in number * 2
main = do
input <- getLine
putStrLn $ "Double your " ++ input
putStrLn $ show $ doubleIt input
簡單來說,你要嘛是純綷、無副作用的函式,要嘛就得是非純綷、具副作用的函式,想要假裝無副作用,而實際上裏頭又有具副作用的函式是行不通的。
這就是 Haskell 將程式中純綷與非純綷部份切割開來的作法,如果你想要做一些非純綷的動作,那麼你就得在非純綷的函式中進行,然後取得值,丟到純綷的函式中去做運算,結果出來後,若想與外界溝通,那就還是得在非純綷的函式中進行。
之後若認識了 Monad ,你會知道為什麼有這種限制。
舉例來說,程式初學者在練習〈河內塔〉時,經常會一邊遞迴,一邊顯示目前盤子的情況,在 Haskell 中方式也不是行不通,例如:
hanoi 1 a _ c = printf "Move from %c to %c\n" a c
hanoi n a b c = do
hanoi (n - 1) a c b
hanoi 1 a b c
hanoi (n - 1) b a c
main = do
putStrLn "Please enter a number: "
n <- getLine
hanoi (read n) 'A' 'B' 'C'
這麼一來,就這個小程式而言,整個世界就變成非純綷了,這麼一來,你就得不到純綷、非副作用的好處,你得學著區分純綷與非純綷,並試著讓非純綷的部份越少越好,這樣你才能享受越多純綷的好處。
最後要出的功課是,試著將上面的河內塔練習的 hanoi 改成純綷、無副作用的函式吧!
後續 >> Haskell Tutorial(21)來寫些迴圈吧!
|