在命令式的語言中,通常會有 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
就可以了。