Haskell Tutorial(20)初探 IO 型態 << 前情
在命令式的語言中,通常會有 for 、while 等,Haskell 中沒有這類迴圈語法,這不意外,迴圈的本質就是變動的(Mutable),使用迴圈,多半是為了改變狀態,無論是變數的狀態、物件的狀態、程式的狀態或者是真實世界的狀態。
不過,在 Haskell 中,可以使用函式來自訂一些類似迴圈的東西,在這之前,得先來認識 return 函式的應用。
使用 return 函式
在 Haskell 中的 return 是個函式,而不是像 C/C++、Java 這類主流語言中常見的語法關鍵字,作用也大相徑庭,C/C++、Java 這類主流語言中的 return ,是用來從函式中返回,如果有指定值的話,就是函式的傳回值,不過,Haskell 中的 return ,接受一個值,然後傳回一個 Monad :
Monad 是個 Typeclass,這樣的函式可以做什麼用?這要看你的 Monad 是什麼!別對 Monad 感到太驚恐,我們一步一步來 …
在〈Haskell Tutorial(20)初探 IO 型態〉中才剛看過 IO 型態,你應該知道了,如果你要透過 getLine 取得使用者輸入,那輸入的值會裝在 IO 中,然後以 IO String 型態的值從 getLine 回傳,那麼要怎麼將值裝在 IO 中?它的值建構式沒有導出,你不能使用 IO "Text" 這樣的方式,也不能使用模式比對取得,對於 IO ,Haskell 是使用 <- 來綁定值。
這段描述跟 return 有什麼關係?IO 型態具有 Monad 的行為,要將值裝在 IO 中,可以使用 return ,例如:
看到了嗎?你可以使用 return "Text" 傳回一個 IO String 的值,接著使用 <- 綁定 IO 中的值,就 IO 來說,return 就好比 <- 的相反,不過,return 並不只能用在 IO ,舉例來說,Maybe 也具有 Monad 的行為,因此,除了使用 Just "Text" 傳回一個 Maybe String 之外,你也可以這麼做:
因為 Maybe 有導出值建構式,因此,上面可以直接使用模式比對來取得 Maybe String 中的值,關於 Monad ,之後還會正式介紹,接下來還是先著重在 return 與 IO ,它們有什麼關係呢?如果你要寫個 echo 程式,重複讀取使用者輸入,直到輸入某個特定字串後結束的話,要怎麼寫呢?
echoUntil :: String -> IO ()
echoUntil str = do
input <- getLine
if input /= str
then do
putStrLn (">> " ++ input)
echoUntil str
else return ()
main = echoUntil "quit"
在 echoUntil 函式中,如果輸入不為 str ,那麼就顯示輸入並繼續遞迴呼叫,如果為 str ,那麼就 return () ,這就傳回一個 IO () ,也就是 IO 中裝著一個 空的 Tuple,putStrLn 的傳回型態也是 IO () ,這樣型態就一致了,因此 echoUntil 的型態就是 String -> IO () 。
可以再來看個 return 與 IO 的應用,在 Haskell 中,putStr 可以輸出一個字串,而不換行,實際上,它是使用 putChar 實作出來,顧名思義,putChar 就是輸出一個字元,來看看它怎麼實作 putStr :
putStr :: String -> IO ()
putStr [] = return ()
putStr (x:xs) = do
putChar x
putStr xs
來寫個 while 迴圈
echoUntil 函式會重複讀取使用者輸入,直到輸入某個特定字串後結束,也許你也會寫個函式重複讀取檔案中每一行,直到讀到某行後結束,或者你還會寫個讀取字元的函式,從網路接受字元,直到某個字元出現後結束 …
(putStr 其實也是類似結構,是一直取得清單中首元素字元,直到空清單為止,只不過用了模式比對語法!)
只要寫幾個這類函式,就會發現這個結構一直出現:
if something
then do
-- 一些 IO Action
else return ()
這時就是該將這個結構封裝起來了:
while :: Bool -> IO () -> IO ()
while cond value = do
if cond then value
else return ()
有了這個 while 函式,之前的 echoUtil 函式就可以改寫為:
echoUntil :: String -> IO ()
echoUntil str = do
input <- getLine
while (input /= str) $ do
putStrLn (">> " ++ input)
echoUntil str
在〈Haskell Tutorial(20)初探 IO 型態〉中,do 定義了一個函式呼叫,因為惰性的關係,這個函式呼叫在真正需要之前並不會被執行,因此,看來就真的像是命令式語言中的 while 迴圈語法。
實際上,Haskell 的 Control.Monad 模組中,就有提供一個 when 函式,不過它的型態是 Monad m => Bool -> m () -> m () ,這比我們方才定義的 while 通用多了,因為我們的 while 只能接受 IO () 與傳回 IO () ,可以使用 when 來改寫上頭的程式:
import Control.Monad
echoUntil :: String -> IO ()
echoUntil str = do
input <- getLine
when (input /= str) $ do
putStrLn (">> " ++ input)
echoUntil str
main = echoUntil "quit"
無窮迴圈
如果你需要個無窮迴圈,可令 when 第一個引數為 True ,例如:
echo :: IO ()
echo = do
when True $ do
input <- getLine
putStrLn (">> " ++ input)
echo
不過,總是要記得呼叫函式本身,這時有個 forever 函式可以幫忙:
import Control.Monad
echo :: IO ()
echo = do
forever $ do
input <- getLine
putStrLn (">> " ++ input)
forever 的實作不難,你可以試試看,除此之外,Haskell 中還有 sequence 、mapM 、forM 等函式,使用方式可參考 輸入與輸出,瞭解了它們的使用方式之後,接下來的功課就是,試著自己實現出來,因為是習題,只要能套用在 IO 就可以了。
後續 >> Haskell Tutorial(22)Maybe 有無、Either 對錯
|