Haskell Tutorial(21)來寫些迴圈吧!


在命令式的語言中,通常會有 forwhile 等,Haskell 中沒有這類迴圈語法,這不意外,迴圈的本質就是變動的(Mutable),使用迴圈,多半是為了改變狀態,無論是變數的狀態、物件的狀態、程式的狀態或者是真實世界的狀態。

不過,在 Haskell 中,可以使用函式來自訂一些類似迴圈的東西,在這之前,得先來認識 return 函式的應用。

使用 return 函式

在 Haskell 中的 return 是個函式,而不是像 C/C++、Java 這類主流語言中常見的語法關鍵字,作用也大相徑庭,C/C++、Java 這類主流語言中的 return,是用來從函式中返回,如果有指定值的話,就是函式的傳回值,不過,Haskell 中的 return,接受一個值,然後傳回一個 Monad

return 的型態

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

看到了嗎?你可以使用 return "Text" 傳回一個 IO String 的值,接著使用 <- 綁定 IO 中的值,就 IO 來說,return 就好比 <- 的相反,不過,return 並不只能用在 IO,舉例來說,Maybe 也具有 Monad 的行為,因此,除了使用 Just "Text" 傳回一個 Maybe String 之外,你也可以這麼做:

return

因為 Maybe 有導出值建構式,因此,上面可以直接使用模式比對來取得 Maybe String 中的值,關於 Monad,之後還會正式介紹,接下來還是先著重在 returnIO,它們有什麼關係呢?如果你要寫個 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 ()

可以再來看個 returnIO 的應用,在 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 中還有 sequencemapMforM 等函式,使用方式可參考 輸入與輸出,瞭解了它們的使用方式之後,接下來的功課就是,試著自己實現出來,因為是習題,只要能套用在 IO 就可以了。