來寫些迴圈吧!?
February 10, 2022在命令式的語言中,通常會有 for
、while
等迴圈語法,Haskell 沒有這類語法,這不意外,迴圈的本質就是變動的(Mutable),使用迴圈就會有副作用,基本上是為了改變狀態,無論是變數狀態、物件狀態、程式狀態或是真實世界的狀態。
不過在 Haskell,可以使用函式來自訂一些長得像迴圈的東西,在這之前,得先來認識 return
函式的應用。
return 函式
C/C++、Java 這類主流語言的 return
,是用來從函式中返回,如果有指定值的話,就是函式的傳回值;不過,Haskell 的 return
是個函式,接受一個值,然後傳回一個 Monad
:
ghci> :t return
return :: Monad m => a -> m a
ghci>
Monad
是個型態類別,別對 Monad
感到太驚恐,我們一步一步來 …
在〈初探 IO 型態〉看過 IO
型態,要透過 getLine
取得使用者輸入,那個你無法控制狀態的世界,會將值裝入 IO
,然後以 IO String
型態從 getLine
傳回,那麼要怎麼將值裝入 IO
?它的值建構式沒有導出,不能使用 IO "Text"
這樣的方式,也不能使用模式比對取得,對於 IO
,Haskell 是使用 <-
來綁定值。
這段描述跟 return
有什麼關係?IO
型態具有 Monad
的行為,要將值裝入 Monad
,可以使用 return
,例如:
ghci> let ms = return "Text"
ghci> :t ms
ms :: Monad m => m String
ghci> text <- ms
ghci> text
"Text"
ghci>
return "Text"
傳回 Monad m => m String
型態的值,接著使用 <-
綁定 Monad
的值,就 Monad
來說,return
就好比 <-
的相反。
舉例來說,Maybe
也具有 Monad
的行為,除了使用 Just "Text"
傳回一個 Maybe String
之外,也可以這麼做:
ghci> let maybe = (return "Text") :: Maybe String
ghci> :t maybe
maybe :: Maybe String
ghci> let (Just text) = maybe
ghci> text
"Text"
ghci>
Maybe
有導出值建構式,因此可以直接使用模式比對來取得 Maybe String
的值,然而實際上,也可以透過 <-
將 Maybe
裡的值綁定到變數,因為這是 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 ()
,return ()
傳回了 IO ()
,也就是 IO
裝著一個空的 tuple,putStrLn
的傳回型態也是 IO ()
,這樣型態就一致了。
再來看個 return
與 IO
的應用,Haskell 的 putStr
可以輸出一個字串而不換行,實際上,它是使用 putChar
實作出來,顧名思義,putChar
就是輸出一個字元,來看看它怎麼實作 putStr
:
putStr :: String -> IO ()
putStr [] = return ()
putStr (x:xs) = do
putChar x
putStr xs
來個 while 迴圈
方才的 echoUntil
函式會重複讀取使用者輸入,直到輸入某個特定字串後結束,也許你也會寫個函式重複讀取檔案中每一行,直到讀到某行後結束,或者你還會寫個讀取字元的函式,從網路接受字元,直到某個字元出現後結束 …
只要寫幾個這類函式,就會發現這個結構一直出現:
if something
then do
-- 一些 IO Action
else return ()
這時就是該將這個結構封裝起來了:
while :: Monad m => Bool -> m () -> m ()
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
在〈初探 IO 型態〉談過,「可以先將 do
理解為,逐層銜接一組 IO
,do
最後呼叫函式的傳回型態,就是 do
的傳回型態」,因為惰性的關係,這個 IO
在真正需要前不會估值,因此,看來就真的像是命令式語言中的 while
迴圈語法。
Haskell 的 Control.Monad
模組中,就有提供 when
函式,可以使用 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)
處理一組 Monad
如果你有個 list,想要逐一將結果顯示怎麼辦?例如,System.Environment
的 getArgs
,可以取得使用者指定的命令列引數,若想逐一顯示呢?基本上可以如下遞迴處理:
import System.Environment
printAll :: [String] -> IO ()
printAll [] = return ()
printAll (x:xs) = do
putStrLn x
printAll xs
main = do
args <- getArgs
printAll args
不過來換個角度想想,putStrLn
會傳回 IO ()
,也就說可以裝載到 list 裡,像是 [putStrLn arg | arg <- args]
,這麼一來 list 裡就全是 IO ()
,不過因為惰性的關係,還不會真正執行,必須將這串 IO ()
用 do
串起來:
import System.Environment
sequence' :: Monad m => [m a] -> m ()
sequence' [] = return ()
sequence' (x:xs) = do
x
sequence' xs
main = do
args <- getArgs
sequence' [putStrLn arg | arg <- args]
其實 Haskell 本身就內建了 sequence
,它是從 Control.Monad
匯入至 Prelude
,這麼寫就可以了:
import System.Environment
main = do
args <- getArgs
sequence [putStrLn arg | arg <- args]
那個 Comprehension 表示式可以使用 map
取代:
import System.Environment
main = do
args <- getArgs
sequence $ map putStrLn args
Control.Monad
有個 mapM
做了類似的事情,它匯入 Prelude
,這麼寫就可以了:
import System.Environment
main = do
args <- getArgs
mapM putStrLn args
Control.Monad
有個 forM
,它的參數與 mapM
相反,這表示 args
可以放在 forM
後面,這能形成像是命令列語言的 foreach 風格:
import System.Environment
import Control.Monad
main = do
args <- getArgs
forM args $ \arg -> do
putStrLn arg
因此想模仿命令式語言,用 for
來遞增索引,就可以如下:
import System.Environment
import Control.Monad
main = do
args <- getArgs
forM [0..length args - 1] $ \i -> do
putStrLn $ args !! i
重複 n 次動作
如果只是想重複執行 n 次動作,雖然也可以如下:
import System.Environment
import Control.Monad
main = do
let n = 3
forM [0..n - 1] $ \_ -> do
putStrLn "Hello, World"
不過使用 Control.Monad
的 replicateM
會更方便:
import System.Environment
import Control.Monad
main = do
let n = 3
replicateM n $ do
putStrLn "Hello, World"
更多 Control.Monad
模組的函式介紹,可參考〈輸入與輸出〉。