http://wiki.jikexueyuan.com/project/haskell-guide/images/dognap.png" alt="" />
我們已經(jīng)說明了 Haskell 是一個(gè)純粹函數(shù)式語言。雖說在命令式語言中我們習(xí)慣給電腦執(zhí)行一連串指令,在函數(shù)式語言中我們是用定義東西的方式進(jìn)行。在 Haskell 中,一個(gè)函數(shù)不能改變狀態(tài),像是改變一個(gè)變數(shù)的內(nèi)容。(當(dāng)一個(gè)函數(shù)會(huì)改變狀態(tài),我們說這函數(shù)是有副作用的。)在 Haskell 中函數(shù)唯一可以做的事是根據(jù)我們給定的參數(shù)來算出結(jié)果。如果我們用同樣的參數(shù)呼叫兩次同一個(gè)函數(shù),它會(huì)回傳相同的結(jié)果。盡管這從命令式語言的角度來看是蠻大的限制,我們已經(jīng)看過它可以達(dá)成多么酷的效果。在一個(gè)命令式語言中,程式語言沒辦法給你任何保證在一個(gè)簡(jiǎn)單如打印出幾個(gè)數(shù)字的函數(shù)不會(huì)同時(shí)燒掉你的房子,綁架你的狗并刮傷你車子的烤漆。例如,當(dāng)我們要建立一棵二元樹的時(shí)候,我們并不插入一個(gè)節(jié)點(diǎn)來改變?cè)械臉洹S捎谖覀儫o法改變狀態(tài),我們的函數(shù)實(shí)際上回傳了一棵新的二元樹。
函數(shù)無法改變狀態(tài)的好處是它讓我們促進(jìn)了我們理解程式的容易度,但同時(shí)也造成了一個(gè)問題。假如說一個(gè)函數(shù)無法改變現(xiàn)實(shí)世界的狀態(tài),那它要如何打印出它所計(jì)算的結(jié)果?畢竟要告訴我們結(jié)果的話,它必須要改變輸出裝置的狀態(tài)(譬如說螢?zāi)唬缓髲奈災(zāi)粋鬟_(dá)到我們的腦,并改變我們心智的狀態(tài)。
不要太早下結(jié)論,Haskell 實(shí)際上設(shè)計(jì)了一個(gè)非常聰明的系統(tǒng)來處理有副作用的函數(shù),它漂亮地將我們的程式區(qū)分成純粹跟非純粹兩部分。非純粹的部分負(fù)責(zé)跟鍵盤還有螢?zāi)粶贤āS辛诉@區(qū)分的機(jī)制,在跟外界溝通的同時(shí),我們還是能夠有效運(yùn)用純粹所帶來的好處,像是惰性求值、容錯(cuò)性跟模組性。
http://wiki.jikexueyuan.com/project/haskell-guide/images/helloworld.png" alt="" />
到目前為止我們都是將函數(shù)載入 GHCi 中來測(cè)試,像是標(biāo)準(zhǔn)函式庫中的一些函式。但現(xiàn)在我們要做些不一樣的,寫一個(gè)真實(shí)跟世界互動(dòng)的 Haskell 程式。當(dāng)然不例外,我們會(huì)來寫個(gè) "hello world"。
現(xiàn)在,我們把下一行打到你熟悉的編輯器中
main = putStrLn "hello, world"
我們定義了一個(gè) main,并在里面以 "hello, world" 為參數(shù)呼叫了 putStrLn??雌饋頉]什么大不了,但不久你就會(huì)發(fā)現(xiàn)它的奧妙。把這程式存成 helloworld.hs。
現(xiàn)在我們將做一件之前沒做過的事:編譯你的程式。打開你的終端并切換到包含 helloworld.hs 的目錄,并輸入下列指令。
$ ghc --make helloworld
[1 of 1] Compiling Main ( helloworld.hs, hellowowlrd.o )
Linking helloworld ...
順利的話你就會(huì)得到如上的訊息,接著你便可以執(zhí)行你的程式 ./helloworld
$ ./helloworld
hello, world
這就是我們第一個(gè)編譯成功并打印出字串到螢?zāi)坏某淌?。很?jiǎn)單吧。
讓我們來看一下我們究竟做了些什么,首先來看一下 putStrLn 函數(shù)的型態(tài):
ghci> :t putStrLn
putStrLn :: String -> IO ()
ghci> :t putStrLn "hello, world"
putStrLn "hello, world" :: IO ()
我們可以這么解讀 putStrLn 的型態(tài):putStrLn 接受一個(gè)字串并回傳一個(gè) I/O action,這 I/O action 包含了 () 的型態(tài)。(即空的 tuple,或者是 unit 型態(tài))。一個(gè) I/O action 是一個(gè)會(huì)造成副作用的動(dòng)作,常是指讀取輸入或輸出到螢?zāi)唬瑫r(shí)也代表會(huì)回傳某些值。在螢?zāi)淮蛴〕鰩讉€(gè)字串并沒有什么有意義的回傳值可言,所以這邊用一個(gè) () 來代表。
那究竟 I/O action 會(huì)在什么時(shí)候被觸發(fā)呢?這就是 main 的功用所在。一個(gè) I/O action 會(huì)在我們把它綁定到 main 這個(gè)名字并且執(zhí)行程式的時(shí)候觸發(fā)。
把整個(gè)程式限制在只能有一個(gè) I/O action 看似是個(gè)極大的限制。這就是為什么我們需要 do 表示法來將所有 I/O action 綁成一個(gè)。來看看下面這個(gè)例子。
main = do
putStrLn "Hello, what's your name?"
name <- getLine
putStrLn ("Hey " ++ name ++ ", you rock!")
新的語法,有趣吧!它看起來就像一個(gè)命令式的程式。如果你編譯并執(zhí)行它,它便會(huì)照你預(yù)期的方式執(zhí)行。我們寫了一個(gè) do 并且接著一連串指令,就像寫個(gè)命令式程式一般,每一步都是一個(gè) I/O action。將所有 I/O action 用 do 綁在一起變成了一個(gè)大的 I/O action。這個(gè)大的 I/O action 的型態(tài)是 IO (),這完全是由最后一個(gè) I/O action 所決定的。
這就是為什么 main 的型態(tài)永遠(yuǎn)都是 main :: IO something,其中 something 是某個(gè)具體的型態(tài)。按照慣例,我們通常不會(huì)把 main 的型態(tài)在程式中寫出來。
另一個(gè)有趣的事情是第三行 name <- getLine。它看起來像是從輸入讀取一行并存到一個(gè)變數(shù) name 之中。真的是這樣嗎?我們來看看 getLine 的型態(tài)吧
ghci> :t getLine
getLine :: IO String
http://wiki.jikexueyuan.com/project/haskell-guide/images/luggage.png" alt="" />
我們可以看到 getLine 是一個(gè)回傳 String 的 I/O action。因?yàn)樗鼤?huì)等使用者輸入某些字串,這很合理。那 name <- getLine 又是如何?你能這樣解讀它:執(zhí)行一個(gè) I/O action getLine 并將它的結(jié)果綁定到 name 這個(gè)名字。getLine 的型態(tài)是 IO String,所以 name 的型態(tài)會(huì)是 String。你能把 I/O action 想成是一個(gè)長(zhǎng)了腳的盒子,它會(huì)跑到真實(shí)世界中替你做某些事,像是在墻壁上涂鴉,然后帶回來某些資料。一旦它帶了某些資料給你,打開盒子的唯一辦法就是用 <-。而且如果我們要從 I/O action 拿出某些資料,就一定同時(shí)要在另一個(gè) I/O action 中。這就是 Haskell 如何漂亮地分開純粹跟不純粹的程式的方法。getLine 在這樣的意義下是不純粹的,因?yàn)閳?zhí)行兩次的時(shí)候它沒辦法保證會(huì)回傳一樣的值。這也是為什么它需要在一個(gè) IO 的型態(tài)建構(gòu)子中,那樣我們才能在 I/O action 中取出資料。而且任何一段程式一旦依賴著 I/O 資料的話,那段程式也會(huì)被視為 I/O code。
但這不表示我們不能在純粹的程式碼中使用 I/O action 回傳的資料。只要我們綁定它到一個(gè)名字,我們便可以暫時(shí)地使用它。像在 name <- getLine 中 name 不過是一個(gè)普通字串,代表在盒子中的內(nèi)容。我們能將這個(gè)普通的字串傳給一個(gè)極度復(fù)雜的函數(shù),并回傳你一生會(huì)有多少財(cái)富。像是這樣:
main = do
putStrLn "Hello, what's your name?"
name <- getLine
putStrLn $ "Read this carefully, because this is your future: " ++ tellFortune name
tellFortune 并不知道任何 I/O 有關(guān)的事,它的型態(tài)只不過是 String -> String。
再來看看這段程式碼吧,他是合法的嗎?
nameTag = "Hello, my name is " ++ getLine
如果你回答不是,恭喜你。如果你說是,你答錯(cuò)了。這么做不對(duì)的理由是 ++ 要求兩個(gè)參數(shù)都必須是串列。他左邊的參數(shù)是 String,也就是 [Char]。然而 getLine 的型態(tài)是 IO String。你不能串接一個(gè)字串跟 I/O action。我們必須先把 String 的值從 I/O action 中取出,而唯一可行的方法就是在 I/O action 中使用 name <- getLine。如果我們需要處理一些非純粹的資料,那我們就要在非純粹的環(huán)境中做。所以我們最好把 I/O 的部分縮減到最小的比例。
每個(gè) I/O action 都有一個(gè)值封裝在里面。這也是為什么我們之前的程式可以這么寫:
main = do
foo <- putStrLn "Hello, what's your name?"
name <- getLine
putStrLn ("Hey " ++ name ++ ", you rock!")
然而,foo 只會(huì)有一個(gè) () 的值,所以綁定到 foo 這個(gè)名字似乎是多余的。另外注意到我們并沒有綁定最后一行的 putStrLn 給任何名字。那是因?yàn)樵谝粋€(gè) do block 中,最后一個(gè) action 不能綁定任何名字。我們?cè)谥笾v解 Monad 的時(shí)候會(huì)說明為什么。現(xiàn)在你可以先想成 do block 會(huì)自動(dòng)從最后一個(gè) action 取出值并綁定給他的結(jié)果。
除了最后一行之外,其他在 do 中沒有綁定名字的其實(shí)也可以寫成綁定的形式。所以 putStrLn "BLAH" 可以寫成 _ <- putStrLn "BLAH"。但這沒什么實(shí)際的意義,所以我們寧愿寫成 putStrLn something。
初學(xué)者有時(shí)候會(huì)想錯(cuò)
name = getLine
以為這行會(huì)讀取輸入并給他綁定一個(gè)名字叫 name 但其實(shí)只是把 getLine 這個(gè) I/O action 指定一個(gè)名字叫 name 罷了。記住,要從一個(gè) I/O action 中取出值,你必須要在另一個(gè) I/O action 中將他用 <- 綁定給一個(gè)名字。
I/O actions 只會(huì)在綁定給 main 的時(shí)候或是在另一個(gè)用 do 串起來的 I/O action 才會(huì)執(zhí)行。你可以用 do 來串接 I/O actions,再用 do 來串接這些串接起來的 I/O actions。不過只有最外面的 I/O action 被指定給 main 才會(huì)觸發(fā)執(zhí)行。
喔對(duì),其實(shí)還有另外一個(gè)情況。就是在 GHCi 中輸入一個(gè) I/O action 并按下 Enter 鍵,那也會(huì)被執(zhí)行
ghci> putStrLn "HEEY"
HEEY
就算我們只是在 GHCi 中打幾個(gè)數(shù)字或是呼叫一個(gè)函數(shù),按下 Enter 就會(huì)計(jì)算它并呼叫 show,再用 putStrLn 將字串打印出在終端上。
還記得 let binding 嗎?如果不記得,回去溫習(xí)一下這個(gè)章節(jié)。它們的形式是 let bindings in expression,其中 bindings 是 expression 中的名字、expression 則是被運(yùn)用到這些名字的算式。我們也提到了 list comprehensions 中,in 的部份不是必需的。你能夠在 do blocks 中使用 let bindings 如同在 list comprehensions 中使用它們一樣,像這樣:
import Data.Char
main = do
putStrLn "What's your first name?"
firstName <- getLine
putStrLn "What's your last name?"
lastName <- getLine
let bigFirstName = map toUpper firstName
bigLastName = map toUpper lastName
putStrLn $ "hey " ++ bigFirstName ++ " " ++ bigLastName ++ ", how are you?"
注意我們是怎么編排在 do block 中的 I/O actions,也注意到我們是怎么編排 let 跟其中的名字的,由于對(duì)齊在 Haskell 中并不會(huì)被無視,這么編排才是好的習(xí)慣。我們的程式用 map toUpper firstName 將 "John" 轉(zhuǎn)成大寫的 "JOHN",并將大寫的結(jié)果綁定到一個(gè)名字上,之后在輸出的時(shí)候參考到了這個(gè)名字。
你也許會(huì)問究竟什么時(shí)候要用 <-,什么時(shí)候用 let bindings?記住,<- 是用來運(yùn)算 I/O actions 并將他的結(jié)果綁定到名稱。而 map toUpper firstName 并不是一個(gè) I/O action。他只是一個(gè)純粹的 expression。所以總結(jié)來說,當(dāng)你要綁定 I/O actions 的結(jié)果時(shí)用 <-,而對(duì)于純粹的 expression 使用 let bindings。對(duì)于錯(cuò)誤的 let firstName = getLine,我們只不過是把 getLine 這個(gè) I/O actions 給了一個(gè)不同的名字罷了。最后還是要用 <- 將結(jié)果取出。
現(xiàn)在我們來寫一個(gè)會(huì)一行一行不斷地讀取輸入,并將讀進(jìn)來的字反過來輸出到螢?zāi)簧系某淌?。程式?huì)在輸入空白行的時(shí)候停止。
main = do
line <- getLine
if null line
then return ()
else do
putStrLn $ reverseWords line
main
reverseWords :: String -> String
reverseWords = unwords . map reverse . words
在分析這段程式前,你可以執(zhí)行看看來感受一下程式的運(yùn)行。
首先,我們來看一下 reverseWords。他不過是一個(gè)普通的函數(shù),假如接受了個(gè)字串 "hey there man",他會(huì)先呼叫 words 來產(chǎn)生一個(gè)字的串列 ["hey", "there", "man"]。然后用 reverse 來 map 整個(gè)串列,得到 ["yeh", "ereht", "nam"],接著用 unwords 來得到最終的結(jié)果 "yeh ereht nam"。這些用函數(shù)合成來簡(jiǎn)潔的表達(dá)。如果沒有用函數(shù)合成,那就會(huì)寫成丑丑的樣子 reverseWords st = unwords (map reverse (words st))
那 main 又是怎么一回事呢?首先,我們用 getLine 從終端讀取了一行,并把這行輸入取名叫 line。然后接著一個(gè)條件式 expression。記住,在 Haskell 中 if 永遠(yuǎn)要伴隨一個(gè) else,這樣每個(gè) expression 才會(huì)有值。當(dāng) if 的條件是 true (也就是輸入了一個(gè)空白行),我們便執(zhí)行一個(gè) I/O action,如果 if 的條件是 false,那 else 底下的 I/O action 被執(zhí)行。這也就是說當(dāng) if 在一個(gè) I/O do block 中的時(shí)候,長(zhǎng)的樣子是 if condition then I/O action else I/O action。
我們首先來看一下在 else 中發(fā)生了什么事。由于我們?cè)?else 中只能有一個(gè) I/O action,所以我們用 do 來將兩個(gè) I/O actions 綁成一個(gè),你可以寫成這樣:
else (do
putStrLn $ reverseWords line
main)
這樣可以明顯看到整個(gè) do block 可以看作一個(gè) I/O action,只是比較丑。但總之,在 do block 里面,我們依序呼叫了 getLine 以及 reverseWords,在那之后,我們遞回呼叫了 main。由于 main 也是一個(gè) I/O action,所以這不會(huì)造成任何問題。呼叫 main 也就代表我們回到程式的起點(diǎn)。
那假如 null line 的結(jié)果是 true 呢?也就是說 then 的區(qū)塊被執(zhí)行。我們看一下區(qū)塊里面有 then return ()。如果你是從 C、Java 或 Python 過來的,你可能會(huì)認(rèn)為 return 不過是作一樣的事情便跳過這一段。但很重要的: return 在 Hakell 里面的意義跟其他語言的 return 完全不同!他們有相同的樣貌,造成了許多人搞錯(cuò),但確實(shí)他們是不一樣的。在命令式語言中,return 通常結(jié)束 method 或 subroutine 的執(zhí)行,并且回傳某個(gè)值給呼叫者。在 Haskell 中,他的意義則是利用某個(gè) pure value 造出 I/O action。用之前盒子的比喻來說,就是將一個(gè) value 裝進(jìn)箱子里面。產(chǎn)生出的 I/O action 并沒有作任何事,只不過將 value 包起來而已。所以在 I/O 的情況下來說,return "haha" 的型態(tài)是 IO String。將 pure value 包成 I/O action 有什么實(shí)質(zhì)意義呢?為什么要弄成 IO 包起來的值?這是因?yàn)槲覀円欢ㄒ?else 中擺上某些 I/O action,所以我們才用 return () 做了一個(gè)沒作什么事情的 I/O action。
在 I/O do block 中放一個(gè) return 并不會(huì)結(jié)束執(zhí)行。像下面這個(gè)程式會(huì)執(zhí)行到底。
main = do
return ()
return "HAHAHA"
line <- getLine
return "BLAH BLAH BLAH"
return 4
putStrLn line
所有在程式中的 return 都是將 value 包成 I/O actions,而且由于我們沒有將他們綁定名稱,所以這些結(jié)果都被忽略。我們能用 <- 與 return 來達(dá)到綁定名稱的目的。
main = do
a <- return "hell"
b <- return "yeah!"
putStrLn $ a ++ " " ++ b
可以看到 return 與 <- 作用相反。return 把 value 裝進(jìn)盒子中,而 <- 將 value 從盒子拿出來,并綁定一個(gè)名稱。不過這么做是有些多余,因?yàn)槟憧梢杂?let bindings 來綁定
main = do
let a = "hell"
b = "yeah"
putStrLn $ a ++ " " ++ b
在 I/O do block 中需要 return 的原因大致上有兩個(gè):一個(gè)是我們需要一個(gè)什么事都不做的 I/O action,或是我們不希望這個(gè) do block 形成的 I/O action 的結(jié)果值是這個(gè) block 中的最后一個(gè) I/O action,我們希望有一個(gè)不同的結(jié)果值,所以我們用 return 來作一個(gè) I/O action 包了我們想要的結(jié)果放在 do block 的最后。
在我們接下去講檔案之前,讓我們來看看有哪些實(shí)用的函數(shù)可以處理 I/O。
putStr 跟 putStrLn 幾乎一模一樣,都是接受一個(gè)字串當(dāng)作參數(shù),并回傳一個(gè) I/O action 打印出字串到終端上,只差在 putStrLn 會(huì)換行而 putStr 不會(huì)罷了。
main = do putStr "Hey, "
putStr "I'm "
putStrLn "Andy!"
$ runhaskell putstr_test.hs
Hey, I'm Andy!
他的 type signature 是 putStr :: String -> IO (),所以是一個(gè)包在 I/O action 中的 unit。也就是空值,沒有辦法綁定他。
putChar 接受一個(gè)字元,并回傳一個(gè) I/O action 將他打印到終端上。
main = do putChar 't'
putChar 'e'
putChar 'h'
$ runhaskell putchar_test.hs
teh
putStr 實(shí)際上就是 putChar 遞回定義出來的。putStr 的邊界條件是空字串,所以假設(shè)我們打印一個(gè)空字串,那他只是回傳一個(gè)什么都不做的 I/O action,像 return ()。如果打印的不是空字串,那就先用 putChar 打印出字串的第一個(gè)字元,然后再用 putStr 打印出字串剩下部份。
putStr :: String -> IO ()
putStr [] = return ()
putStr (x:xs) = do
putChar x
putStr xs
看看我們?nèi)绾卧?I/O 中使用遞回,就像我們?cè)?pure code 中所做的一樣。先定義一個(gè)邊界條件,然后再思考剩下如何作。
print 接受任何是 Show typeclass 的 instance 的型態(tài)的值,這代表我們知道如何用字串表示他,呼叫 show 來將值變成字串然后將其輸出到終端上?;旧希褪?putStrLn . show。首先呼叫 show 然后把結(jié)果喂給 putStrLn,回傳一個(gè) I/O action 打印出我們的值。
main = do print True
print 2
print "haha"
print 3.2
print [3,4,3]
$ runhaskell print_test.hs
True
2
"haha"
3.2
[3,4,3]
就像你看到的,這是個(gè)很方便的函數(shù)。還記得我們提到 I/O actions 只有在 main 中才會(huì)被執(zhí)行以及在 GHCI 中運(yùn)算的事情嗎?當(dāng)我們用鍵盤打了些值,像 3 或 [1,2,3] 并按下 Enter,GHCI 實(shí)際上就是用了 print 來將這些值輸出到終端。
ghci> 3
3
ghci> print 3
3
ghci> map (++"!") ["hey","ho","woo"]
["hey!","ho!","woo!"]
ghci> print (map (++"!") ["hey", "ho", "woo"])
["hey!","ho!","woo!"]
當(dāng)我們需要打印出字串,我們會(huì)用 putStrLn,因?yàn)槲覀儾幌胍車幸?hào),但對(duì)于輸出值來說,print 才是最常用的。
getChar 是一個(gè)從輸入讀進(jìn)一個(gè)字元的 I/O action,因此他的 type signature 是 getChar :: IO Char,代表一個(gè) I/O action 的結(jié)果是 Char。注意由于緩沖區(qū)的關(guān)系,只有當(dāng) Enter 被按下的時(shí)候才會(huì)觸發(fā)讀取字元的行為。
main = do
c <- getChar
if c /= ' '
then do
putChar c
main
else return ()
這程式看起來像是讀取一個(gè)字元并檢查他是否為一個(gè)空白。如果是的話便停止,如果不是的話便打印到終端上并重復(fù)之前的行為。在某種程度上來說也不能說錯(cuò),只是結(jié)果不如你預(yù)期而已。來看看結(jié)果吧。
$ runhaskell getchar_test.hs
hello sir
hello
上面的第二行是輸入。我們輸入了 hello sir 并按下了 Enter。由于緩沖區(qū)的關(guān)系,程式是在我們按了 Enter 后才執(zhí)行而不是在某個(gè)輸入字元的時(shí)候。一旦我們按下了 Enter,那他就把我們直到目前輸入的一次做完。
when 這函數(shù)可以在 Control.Monad 中找到他 (你必須 import Contorl.Monad 才能使用他)。他在一個(gè) do block 中看起來就像一個(gè)控制流程的 statement,但實(shí)際上他的確是一個(gè)普通的函數(shù)。他接受一個(gè) boolean 值跟一個(gè) I/O action。如果 boolean 值是 True,便回傳我們傳給他的 I/O action。如果 boolean 值是 False,便回傳 return (),即什么都不做的 I/O action。我們接下來用 when 來改寫我們之前的程式。
import Control.Monad
main = do
c <- getChar
when (c /= ' ') $ do
putChar c
main
就像你看到的,他可以將 if something then do some I/O action else return () 這樣的模式封裝起來。
sequence 接受一串 I/O action,并回傳一個(gè)會(huì)依序執(zhí)行他們的 I/O action。運(yùn)算的結(jié)果是包在一個(gè) I/O action 的一連串 I/O action 的運(yùn)算結(jié)果。他的 type signature 是 sequence :: [IO a] -> IO [a]
main = do
a <- getLine
b <- getLine
c <- getLine
print [a,b,c]
其實(shí)可以寫成
main = do
rs <- sequence [getLine, getLine, getLine]
print rs
所以 sequence [getLine, getLine, getLine] 作成了一個(gè)執(zhí)行 getLine 三次的 I/O action。如果我們對(duì)他綁定一個(gè)名字,結(jié)果便是這串結(jié)果的串列。也就是說,三個(gè)使用者輸入的東西組成的串列。
一個(gè)常見的使用方式是我們將 print 或 putStrLn 之類的函數(shù) map 到串列上。map print [1,2,3,4] 這個(gè)動(dòng)作并不會(huì)產(chǎn)生一個(gè) I/O action,而是一串 I/O action,就像是 [print 1, print 2, print 3, print 4]。如果我們將一串 I/O action 變成一個(gè) I/O action,我們必須用 sequence
ghci> sequence (map print [1,2,3,4,5])
1
2
3
4
5
[(),(),(),(),()]
那 [(),(),(),(),()] 是怎么回事?當(dāng)我們?cè)?GHCI 中運(yùn)算 I/O action,他會(huì)被執(zhí)行并把結(jié)果打印出來,唯一例外是結(jié)果是 () 的時(shí)候不會(huì)被打印出。這也是為什么 putStrLn "hehe" 在 GHCI 中只會(huì)打印出 hehe(因?yàn)?putStrLn "hehe" 的結(jié)果是 ())。但當(dāng)我們使用 getLine 時(shí),由于 getLine 的型態(tài)是 IO String,所以結(jié)果會(huì)被打印出來。
由于對(duì)一個(gè)串列 map 一個(gè)回傳 I/O action 的函數(shù),然后再 sequence 他這個(gè)動(dòng)作太常用了。所以有一些函數(shù)在函式庫中 mapM 跟 mapM_。mapM 接受一個(gè)函數(shù)跟一個(gè)串列,將對(duì)串列用函數(shù) map 然后 sequence 結(jié)果。mapM_ 也作同樣的事,只是他把運(yùn)算的結(jié)果丟掉而已。在我們不關(guān)心 I/O action 結(jié)果的情況下,mapM_ 是最常被使用的。
ghci> mapM print [1,2,3]
1
2
3
[(),(),()]
ghci> mapM_ print [1,2,3]
1
2
3
forever 接受一個(gè) I/O action 并回傳一個(gè)永遠(yuǎn)作同一件事的 I/O action。你可以在 Control.Monad 中找到他。下面的程式會(huì)不斷地要使用者輸入些東西,并把輸入的東西轉(zhuǎn)成大寫輸出到螢?zāi)簧稀?/p>
import Control.Monad
import Data.Char
main = forever $ do
putStr "Give me some input: "
l <- getLine
putStrLn $ map toUpper l
在 Control.Monad 中的 forM 跟 mapM 的作用一樣,只是參數(shù)的順序相反而已。第一個(gè)參數(shù)是串列,而第二個(gè)則是函數(shù)。這有什么用?在一些有趣的情況下還是有用的:
import Control.Monad
main = do
colors <- forM [1,2,3,4] (\a -> do
putStrLn $ "Which color do you associate with the number " ++ show a ++ "?"
color <- getLine
return color)
putStrLn "The colors that you associate with 1, 2, 3 and 4 are: "
mapM putStrLn colors
(\a -> do ...) 是接受一個(gè)數(shù)字并回傳一個(gè) I/O action 的函數(shù)。我們必須用括號(hào)括住他,不然 lambda 會(huì)貪心 match 的策略會(huì)把最后兩個(gè) I/O action 也算進(jìn)去。注意我們?cè)?do block 里面 return color。我們那么作是讓 do block 的結(jié)果是我們選的顏色。實(shí)際上我們并不需那么作,因?yàn)?getLine 已經(jīng)達(dá)到我們的目的。先 color <- getLine 再 return color 只不過是把值取出再包起來,其實(shí)是跟 getLine 效果相當(dāng)。forM 產(chǎn)生一個(gè) I/O action,我們把結(jié)果綁定到 colors 這名稱。colors 是一個(gè)普通包含字串的串列。最后,我們用 mapM putStrLn colors 打印出所有顏色。
你可以把 forM 的意思想成將串列中的每個(gè)元素作成一個(gè) I/O action。至于每個(gè) I/O action 實(shí)際作什么就要看原本的元素是什么。然后,執(zhí)行這些 I/O action 并將結(jié)果綁定到某個(gè)名稱上?;蚴侵苯訉⒔Y(jié)果忽略掉。
$ runhaskell from_test.hs
Which color do you associate with the number 1?
white
Which color do you associate with the number 2?
blue
Which color do you associate with the number 3?
red
Which color do you associate with the number 4?
orange
The colors that you associate with 1, 2, 3 and 4 are:
white
blue
red
orange
其實(shí)我們也不是一定要用到 forM,只是用了 forM 程式會(huì)比較容易理解。正常來講是我們需要在 map 跟 sequence 的時(shí)候定義 I/O action 的時(shí)候使用 forM,同樣地,我們也可以將最后一行寫成 forM colors putStrLn。
在這一節(jié),我們學(xué)會(huì)了輸入與輸出的基礎(chǔ)。我們也了解了什么是 I/O action,他們是如何幫助我們達(dá)成輸入與輸出的目的。這邊重復(fù)一遍,I/O action 跟其他 Haskell 中的 value 沒有兩樣。我們能夠把他當(dāng)參數(shù)傳給函式,或是函式回傳 I/O action。他們特別之處在于當(dāng)他們是寫在 main 里面或 GHCI 里面的時(shí)候,他們會(huì)被執(zhí)行,也就是實(shí)際輸出到你螢?zāi)换蜉敵鲆粜У臅r(shí)候。每個(gè) I/O action 也能包著一個(gè)從真實(shí)世界拿回來的值。
不要把像是 putStrLn 的函式想成接受字串并輸出到螢?zāi)?。要想成一個(gè)函式接受字串并回傳一個(gè) I/O action。當(dāng) I/O action 被執(zhí)行的時(shí)候,會(huì)漂亮地打印出你想要的東西。
http://wiki.jikexueyuan.com/project/haskell-guide/images/streams.png" alt="" />
getChar 是一個(gè)讀取單一字元的 I/O action。getLine 是一個(gè)讀取一行的 I/O action。這是兩個(gè)非常直覺的函式,多數(shù)程式語言也有類似這兩個(gè)函式的 statement 或 function。但現(xiàn)在我們來看看 getContents。getContents 是一個(gè)從標(biāo)準(zhǔn)輸入讀取直到 end-of-file 字元的 I/O action。他的型態(tài)是 getContents :: IO String。最酷的是 getContents 是惰性 I/O (Lazy I/O)。當(dāng)我們寫了 foo <- getContents,他并不會(huì)馬上讀取所有輸入,將他們存在 memory 里面。他只有當(dāng)你真的需要輸入資料的時(shí)候才會(huì)讀取。
當(dāng)我們需要重導(dǎo)一個(gè)程式的輸出到另一個(gè)程式的輸入時(shí),getContents 非常有用。假設(shè)我們有下面一個(gè)文字檔:
I'm a lil' teapot
What's with that airplane food, huh?
It's so small, tasteless
還記得我們介紹 forever 時(shí)寫的小程式嗎?會(huì)把所有輸入的東西轉(zhuǎn)成大寫的那一個(gè)。為了防止你忘記了,這邊再重復(fù)一遍。
import Control.Monad
import Data.Char
main = forever $ do
putStr "Give me some input: "
l <- getLine
putStrLn $ map toUpper l
將我們的程式存成 capslocker.hs 然后編譯他。然后用 Unix 的 Pipe 將文字檔喂給我們的程式。我們使用的是 GNU 的 cat,會(huì)將指定的檔案輸出到螢?zāi)弧?/p>
$ ghc --make capslocker
[1 of 1] Compiling Main ( capslocker.hs, capslocker.o )
Linking capslocker ...
$ cat haiku.txt
I'm a lil' teapot
What's with that airplane food, huh?
It's so small, tasteless
$ cat haiku.txt | ./capslocker
I'M A LIL' TEAPOT
WHAT'S WITH THAT AIRPLANE FOOD, HUH?
IT'S SO SMALL, TASTELESS
capslocker <stdin>: hGetLine: end of file
就如你看到的,我們是用 | 這符號(hào)來將某個(gè)程式的輸出 piping 到另一個(gè)程式的輸入。我們做的事相當(dāng)于 run 我們的 capslocker,然后將 haiku 的內(nèi)容用鍵盤打到終端上,最后再按 Ctrl-D 來代表 end-of-file。這就像執(zhí)行 cat haiku.txt 后大喊,嘿,不要把內(nèi)容打印到終端上,把內(nèi)容塞到 capslocker!
我們用 forever 在做的事基本上就是將輸入經(jīng)過轉(zhuǎn)換后變成輸出。用 getContents 的話可以讓我們的程式更加精煉。
import Data.Char
main = do
contents <- getContents
putStr (map toUpper contents)
我們將 getContents 取回的字串綁定到 contents。然后用 toUpper map 到整個(gè)字串后打印到終端上。記住字串基本上就是一串惰性的串列 (list),同時(shí) getContents 也是惰性 I/O,他不會(huì)一口氣讀入內(nèi)容然后將內(nèi)容存在記憶體中。實(shí)際上,他會(huì)一行一行讀入并輸出大寫的版本,這是因?yàn)檩敵霾攀钦娴男枰斎氲馁Y料的時(shí)候。
$ cat haiku.txt | ./capslocker
I'M A LIL' TEAPOT
WHAT'S WITH THAT AIRPLAN FOOD, HUH?
IT'S SO SMALL, TASTELESS
很好,程式運(yùn)作正常。假如我們執(zhí)行 capslocker 然后自己打幾行字呢?
$ ./capslocker
hey ho
HEY HO
lets go
LETS GO
按下 Ctrl-D 來離開環(huán)境。就像你看到的,程式是一行一行將我們的輸入打印出來。當(dāng) getContent 的結(jié)果被綁定到 contents 的時(shí)候,他不是被表示成在記憶體中的一個(gè)字串,反而比較像是他有一天會(huì)是字串的一個(gè)承諾。當(dāng)我們將 toUpper map 到 contents 的時(shí)候,便也是一個(gè)函數(shù)被承諾將會(huì)被 map 到內(nèi)容上。最后 putStr 則要求先前的承諾說,給我一行大寫的字串吧。實(shí)際上還沒有任何一行被取出,所以便跟 contents 說,不如從終端那邊取出些字串吧。這才是 getContents 真正從終端讀入一行并把這一行交給程式的時(shí)候。程式便將這一行用 toUpper 處理并交給 putStr,putStr 則打印出他。之后 putStr 再說:我需要下一行。整個(gè)步驟便再重復(fù)一次,直到讀到 end-of-file 為止。
接著我們來寫個(gè)程式,讀取輸入,并只打印出少于十個(gè)字元的行。
main = do
contents <- getContents
putStr (shortLinesOnly contents)
shortLinesOnly :: String -> String
shortLinesOnly input =
let allLines = lines input
shortLines = filter (\line -> length line < 10) allLines
result = unlines shortLines
in result
我們把 I/O 部份的程式碼弄得很短。由于程式的行為是接某些輸入,作些處理然后輸出。我們可以把他想成讀取輸入,呼叫一個(gè)函數(shù),然后把函數(shù)的結(jié)果輸出。
shortLinesOnly 的行為是這樣:拿到一個(gè)字串,像是 "short\nlooooooooooooooong\nshort again"。這字串有三行,前后兩行比較短,中間一行很常。他用 lines 把字串分成 ["short", "looooooooooooooong", "short again"],并把結(jié)果綁定成 allLines。然后過濾這些字串,只有少于十個(gè)字元的留下,["short", "short again"],最后用 unlines 把這些字串用換行接起來,形成 "short\nshort again"
i'm short
so am i
i am a loooooooooong line!!!
yeah i'm long so what hahahaha!!!!!!
short line
loooooooooooooooooooooooooooong
short
$ ghc --make shortlinesonly
[1 of 1] Compiling Main ( shortlinesonly.hs, shortlinesonly.o )
Linking shortlinesonly ...
$ cat shortlines.txt | ./shortlinesonly
i'm short
so am i
short
我們把 shortlines.txt 的內(nèi)容經(jīng)由 pipe 送給 shortlinesonly,結(jié)果就如你看到,我們只有得到比較短的行。
從輸入那一些字串,經(jīng)由一些轉(zhuǎn)換然后輸出這樣的模式實(shí)在太常用了。常用到甚至建立了一個(gè)函數(shù)叫 interact。interact 接受一個(gè) String -> String 的函數(shù),并回傳一個(gè) I/O action。那個(gè) I/O action 會(huì)讀取一些輸入,呼叫提供的函數(shù),然后把函數(shù)的結(jié)果打印出來。所以我們的程式可以改寫成這樣。
main = interact shortLinesOnly
shortLinesOnly :: String -> String
shortLinesOnly input =
let allLines = lines input
shortLines = filter (\line -> length line < 10) allLines
result = unlines shortLines
in result
我們甚至可以再讓程式碼更短一些,像這樣
main = interact $ unlines . filter ((<10) . length) . lines
看吧,我們讓程式縮到只剩一行了,很酷吧!
能應(yīng)用 interact 的情況有幾種,像是從輸入 pipe 讀進(jìn)一些內(nèi)容,然后丟出一些結(jié)果的程式;或是從使用者獲取一行一行的輸入,然后丟回根據(jù)那一行運(yùn)算的結(jié)果,再拿取另一行。這兩者的差別主要是取決于使用者使用他們的方式。
我們?cè)賮韺懥硪粋€(gè)程式,它不斷地讀取一行行并告訴我們那一行字串是不是一個(gè)回文字串 (palindrome)。我們當(dāng)然可以用 getLine 讀取一行然后再呼叫 main 作同樣的事。不過同樣的事情可以用 interact 更簡(jiǎn)潔地達(dá)成。當(dāng)使用 interact 的時(shí)候,想像你是將輸入經(jīng)有某些轉(zhuǎn)換成輸出。在這個(gè)情況當(dāng)中,我們要將每一行輸入轉(zhuǎn)換成 "palindrome" 或 "not a palindrome"。所以我們必須寫一個(gè)函數(shù)將 "elephant\nABCBA\nwhatever" 轉(zhuǎn)換成 not a palindrome\npalindrome\nnot a palindrome"。來動(dòng)手吧!
respondPalindromes contents = unlines (map (\xs ->
if isPalindrome xs then "palindrome" else "not a palindrome") (lines contents))
where isPalindrome xs = xs == reverse xs
再來將程式改寫成 point-free 的形式
respondPalindromes = unlines . map (\xs ->
if isPalindrome xs then "palindrome" else "not a palindrome") . lines
where isPalindrome xs = xs == reverse xs
很直覺吧!首先將 "elephant\nABCBA\nwhatever" 變成 ["elephant", "ABCBA", "whatever"] 然后將一個(gè) lambda 函數(shù) map 它,["not a palindrome", "palindrome", "not a palindrome"] 然后用 unlines 變成一行字串。接著
main = interact respondPalindromes
來測(cè)試一下吧。
$ runhaskell palindrome.hs
hehe
not a palindrome
ABCBA
palindrome
cookie
not a palindrome
即使我們的程式是把一大把字串轉(zhuǎn)換成另一個(gè),其實(shí)他表現(xiàn)得好像我們是一行一行做的。這是因?yàn)?Haskell 是惰性的,程式想要打印出第一行結(jié)果時(shí),他必須要先有第一行輸入。所以一旦我們給了第一行輸入,他便打印出第一行結(jié)果。我們用 end-of-line 字元來結(jié)束程式。
我們也可以用 pipe 的方式將輸入喂給程式。假設(shè)我們有這樣一個(gè)檔案。
dogaroo
radar
rotor
madam
將他存為 words.txt,將他喂給程式后得到的結(jié)果
$ cat words.txt | runhaskell palindromes.hs
not a palindrome
palindrome
palindrome
palindrome
再一次地提醒,我們得到的結(jié)果跟我們自己一個(gè)一個(gè)字打進(jìn)輸入的內(nèi)容是一樣的。我們看不到 palindrome.hs 輸入的內(nèi)容是因?yàn)閮?nèi)容來自于檔案。
你應(yīng)該大致了解 Lazy I/O 是如何運(yùn)作,并能善用他的優(yōu)點(diǎn)。他可以從輸入轉(zhuǎn)換成輸出的角度方向思考。由于 Lazy I/O,沒有輸入在被用到之前是真的被讀入。
到目前為止,我們的示范都是從終端讀取某些東西或是打印出某些東西到終端。但如果我們想要讀寫檔案呢?其實(shí)從某個(gè)角度來說我們已經(jīng)作過這件事了。我們可以把讀寫終端想成讀寫檔案。只是把檔案命名成 stdout 跟 stdin 而已。他們分別代表標(biāo)準(zhǔn)輸出跟標(biāo)準(zhǔn)輸入。我們即將看到的讀寫檔案跟讀寫終端并沒什么不同。
首先來寫一個(gè)程式,他會(huì)開啟一個(gè)叫 girlfriend.txt 的檔案,檔案里面有 Avril Lavigne 的暢銷名曲 Girlfriend,并將內(nèi)容打印到終端上。接下來是 girlfriend.txt 的內(nèi)容。
Hey! Hey! You! You!
I don't like your girlfriend!
No way! No way!
I think you need a new one!
這則是我們的主程式。
import System.IO
main = do
handle <- openFile "girlfriend.txt" ReadMode
contents <- hGetContents handle
putStr contents
hClose handle
執(zhí)行他后得到的結(jié)果。
$ runhaskell girlfriend.hs
Hey! Hey! You! You!
I don't like your girlfriend!
No way! No way!
I think you need a new one!
我們來一行行看一下程式。我們的程式用 do 把好幾個(gè) I/O action 綁在一起。在 do block 的第一行,我們注意到有一個(gè)新的函數(shù)叫 openFile。他的 type signature 是 openFile :: FilePath -> IOMode -> IO Handle。他說了 openFile 接受一個(gè)檔案路徑跟一個(gè) IOMode,并回傳一個(gè) I/O action,他會(huì)打開一個(gè)檔案并把檔案關(guān)聯(lián)到一個(gè) handle。
FilePath 不過是 String 的 type synonym。
type FilePath = String
IOMode 則是一個(gè)定義如下的型態(tài)
data IOMode = ReadMode | WriteMode | AppendMode | ReadWriteMode
http://wiki.jikexueyuan.com/project/haskell-guide/images/file.png" alt="" />
就像我們之前定義的型態(tài),分別代表一個(gè)星期的七天。這個(gè)型態(tài)代表了我們想對(duì)打開的檔案做什么。很簡(jiǎn)單吧。留意到我們的型態(tài)是 IOMode 而不是 IO Mode。IO Mode 代表的是一個(gè) I/O action 包含了一個(gè)型態(tài)為 Mode 的值,但 IOMode 不過是一個(gè)陽春的 enumeration。
最后,他回傳一個(gè) I/O action 會(huì)將指定的檔案用指定的模式打開。如果我們將 I/O action 綁定到某個(gè)東西,我們會(huì)得到一個(gè) Handle。型態(tài)為 Handle 的值代表我們的檔案在哪里。有了 handle 我們才知道要從哪個(gè)檔案讀取內(nèi)容。想讀取檔案但不將檔案綁定到 handle 上這樣做是很蠢的。所以,我們將一個(gè) handle 綁定到 handle。
接著一行,我們看到一個(gè)叫 hGetContents 的函數(shù)。他接了一個(gè) Handle,所以他知道要從哪個(gè)檔案讀取內(nèi)容并回傳一個(gè) IO String。一個(gè)包含了檔案內(nèi)容的 I/O action。這函數(shù)跟 getContents 差不多。唯一的差別是 getContents 會(huì)自動(dòng)從標(biāo)準(zhǔn)輸入讀取內(nèi)容(也就是終端),而 hGetContents 接了一個(gè) file handle,這 file handle 告訴他讀取哪個(gè)檔案。除此之外,他們都是一樣的。就像 getContents,hGetContents 不會(huì)把檔案一次都拉到記憶體中,而是有必要才會(huì)讀取。這非???,因?yàn)槲覀儼?contents 當(dāng)作是整個(gè)檔案般用,但他實(shí)際上不在記憶體中。就算這是個(gè)很大的檔案,hGetContents 也不會(huì)塞爆你的記憶體,而是只有必要的時(shí)候才會(huì)讀取。
要留意檔案的 handle 還有檔案的內(nèi)容兩個(gè)概念的差異,在我們的程式中他們分別被綁定到 handle 跟 contents 兩個(gè)名字。handle 是我們拿來區(qū)分檔案的依據(jù)。如果你把整個(gè)檔案系統(tǒng)想成一本厚厚的書,每個(gè)檔案分別是其中的一個(gè)章節(jié),handle 就像是書簽一般標(biāo)記了你現(xiàn)在正在閱讀(或?qū)懭耄┠囊粋€(gè)章節(jié),而內(nèi)容則是章節(jié)本身。
我們使用 putStr contents 打印出內(nèi)容到標(biāo)準(zhǔn)輸出,然后我們用了 hClose。他接受一個(gè) handle 然后回傳一個(gè)關(guān)掉檔案的 I/O action。在用了 openFile 之后,你必須自己把檔案關(guān)掉。
要達(dá)到我們目的的另一種方式是使用 withFile,他的 type signature 是 withFile :: FilePath -> IOMode -> (Handle -> IO a) -> IO a。他接受一個(gè)檔案路徑,一個(gè) IOMode 以及一個(gè)函數(shù),這函數(shù)則接受一個(gè) handle 跟一個(gè) I/O action。withFile 最后回傳一個(gè)會(huì)打開檔案,對(duì)檔案作某件事然后關(guān)掉檔案的 I/O action。處理的結(jié)果是包在最后的 I/O action 中,這結(jié)果跟我們給的函數(shù)的回傳是相同的。這聽起來有些復(fù)雜,但其實(shí)很簡(jiǎn)單,特別是我們有 lambda,來看看我們用 withFile 改寫前面程式的一個(gè)范例:
import System.IO
main = do
withFile "girlfriend.txt" ReadMode (\handle -> do
contents <- hGetContents handle
putStr contents)
正如你看到的,程式跟之前的看起來很像。(\handle -> ... ) 是一個(gè)接受 handle 并回傳 I/O action 的函數(shù),他通常都是用 lambda 來表示。我們需要一個(gè)回傳 I/O action 的函數(shù)的理由而不是一個(gè)本身作處理并關(guān)掉檔案的 I/O action,是因?yàn)檫@樣一來那個(gè) I/O action 不會(huì)知道他是對(duì)哪個(gè)檔案在做處理。用 withFile 的話,withFile 會(huì)打開檔案并把 handle 傳給我們給他的函數(shù),之后他則拿到一個(gè) I/O action,然后作成一個(gè)我們描述的 I/O action,最后關(guān)上檔案。例如我們可以這樣自己作一個(gè) withFile:
withFile' :: FilePath -> IOMode -> (Handle -> IO a) -> IO a
withFile' path mode f = do
handle <- openFile path mode
result <- f handle
hClose handle
return result
http://wiki.jikexueyuan.com/project/haskell-guide/images/edd.png" alt="" />
我們知道要回傳的是一個(gè) I/O action,所以我們先放一個(gè) do。首先我們打開檔案,得到一個(gè) handle。然后我們 apply handle 到我們的函數(shù),并得到一個(gè)做事的 I/O action。我們綁定那個(gè) I/O action 到 result 這個(gè)名字,關(guān)上 handle 并 return result。return 的作用把從 f 得到的結(jié)果包在 I/O action 中,這樣一來 I/O action 中就包含了 f handle 得到的結(jié)果。如果 f handle 回傳一個(gè)從標(biāo)準(zhǔn)輸入讀去數(shù)行并寫到檔案然后回傳讀入的行數(shù)的 I/O action,在 withFile' 的情形中,最后的 I/O action 就會(huì)包含讀入的行數(shù)。
就像 hGetContents 對(duì)應(yīng) getContents 一樣,只不過是針對(duì)某個(gè)檔案。我們也有 hGetLine、hPutStr、hPutStrLn、hGetChar 等等。他們分別是少了 h 的那些函數(shù)的對(duì)應(yīng)。只不過他們要多拿一個(gè) handle 當(dāng)參數(shù),并且是針對(duì)特定檔案而不是標(biāo)準(zhǔn)輸出或標(biāo)準(zhǔn)輸入。像是 putStrLn 是一個(gè)接受一個(gè)字串并回傳一個(gè)打印出加了換行字元的字串的 I/O action 的函數(shù)。hPutStrLn 接受一個(gè) handle 跟一個(gè)字串,回傳一個(gè)打印出加了換行字元的字串到檔案的 I/O action。以此類推,hGetLine 接受一個(gè) handle 然后回傳一個(gè)從檔案讀取一行的 I/O action。
讀取檔案并對(duì)他們的字串內(nèi)容作些處理實(shí)在太常見了,常見到我們有三個(gè)函數(shù)來更進(jìn)一步簡(jiǎn)化我們的工作。
readFile 的 type signature 是 readFile :: FilePath -> IO String。記住,FilePath 不過是 String 的一個(gè)別名。readFile 接受一個(gè)檔案路徑,回傳一個(gè)惰性讀取我們檔案的 I/O action。然后將檔案的內(nèi)容綁定到某個(gè)字串。他比起先 openFile,綁定 handle,然后 hGetContents 要好用多了。這邊是一個(gè)用 readFile 改寫之前例子的范例:
import System.IO
main = do
contents <- readFile "girlfriend.txt"
putStr contents
由于我們拿不到 handle,所以我們也無法關(guān)掉他。這件事 Haskell 的 readFile 在背后幫我們做了。
writeFile 的型態(tài)是 writefile :: FilePath -> String -> IO ()。他接受一個(gè)檔案路徑,以及一個(gè)要寫到檔案中的字串,并回傳一個(gè)寫入動(dòng)作的 I/O action。如果這個(gè)檔案已經(jīng)存在了,他會(huì)先把檔案內(nèi)容都砍了再寫入。下面示范了如何把 girlfriend.txt 的內(nèi)容轉(zhuǎn)成大寫然后寫入到 girlfriendcaps.txt 中
import System.IO
import Data.Char
main = do
contents <- readFile "girlfriend.txt"
writeFile "girlfriendcaps.txt" (map toUpper contents)
$ runhaskell girlfriendtocaps.hs
$ cat girlfriendcaps.txt
HEY! HEY! YOU! YOU!
I DON'T LIKE YOUR GIRLFRIEND!
NO WAY! NO WAY!
I THINK YOU NEED A NEW ONE!
appendFile 的型態(tài)很像 writeFile,只是 appendFile 并不會(huì)在檔案存在時(shí)把檔案內(nèi)容砍掉而是接在后面。
假設(shè)我們有一個(gè)檔案叫 todo.txt``,里面每一行是一件要做的事情。現(xiàn)在我們寫一個(gè)程式,從標(biāo)準(zhǔn)輸入接受一行將他加到我們的 to-do list 中。
import System.IO
main = do
todoItem <- getLine
appendFile "todo.txt" (todoItem ++ "\n")
$ runhaskell appendtodo.hs
Iron the dishes
$ runhaskell appendtodo.hs
Dust the dog
$ runhaskell appendtodo.hs
Take salad out of the oven
$ cat todo.txt
Iron the dishes
Dust the dog
Take salad out of the oven
由于 getLine 回傳的值不會(huì)有換行字元,我們需要在每一行最后加上 "\n"。
還有一件事,我們提到 contents <- hGetContents handle 是惰性 I/O,不會(huì)將檔案一次都讀到記憶體中。
所以像這樣寫的話:
main = do
withFile "something.txt" ReadMode (\handle -> do
contents <- hGetContents handle
putStr contents)
實(shí)際上像是用一個(gè) pipe 把檔案弄到標(biāo)準(zhǔn)輸出。正如你可以把 list 想成 stream 一樣,你也可以把檔案想成 stream。他會(huì)每次讀一行然后打印到終端上。你也許會(huì)問這個(gè) pipe 究竟一次可以塞多少東西,讀去硬碟的頻率究竟是多少?對(duì)于文字檔而言,預(yù)設(shè)的 buffer 通常是 line-buffering。這代表一次被讀進(jìn)來的大小是一行。這也是為什么在這個(gè) case 我們是一行一行處理。對(duì)于 binary file 而言,預(yù)設(shè)的 buffer 是 block-buffering。這代表我們是一個(gè) chunk 一個(gè) chunk 去讀得。而一個(gè) chunk 的大小是根據(jù)作業(yè)系統(tǒng)不同而不同。
你能用 hSetBuffering 來控制 buffer 的行為。他接受一個(gè) handle 跟一個(gè) BufferMode,回傳一個(gè)會(huì)設(shè)定 buffer 行為的 I/O action。BufferMode 是一個(gè) enumeration 型態(tài),他可能的值有:NoBuffering, LineBuffering 或 BlockBuffering (Maybe Int)。其中 Maybe Int 是表示一個(gè) chunck 有幾個(gè) byte。如果他的值是 Nothing,則作業(yè)系統(tǒng)會(huì)幫你決定 chunk 的大小。NoBuffering 代表我們一次讀一個(gè) character。一般來說 NoBuffering 的表現(xiàn)很差,因?yàn)樗嫒∮驳念l率很高。
接下來是我們把之前的范例改寫成用 2048 bytes 的 chunk 讀取,而不是一行一行讀。
main = do
withFile "something.txt" ReadMode (\handle -> do
hSetBuffering handle $ BlockBuffering (Just 2048)
contents <- hGetContents handle
putStr contents)
用更大的 chunk 來讀取對(duì)于減少存取硬碟的次數(shù)是有幫助的,特別是我們的檔案其實(shí)是透過網(wǎng)路來存取。
我們也可以使用 hFlush,他接受一個(gè) handle 并回傳一個(gè)會(huì) flush buffer 到檔案的 I/O action。當(dāng)我們使用 line-buffering 的時(shí)候,buffer 在每一行都會(huì)被 flush 到檔案。當(dāng)我們使用 block-buffering 的時(shí)候,是在我們讀每一個(gè) chunk 作 flush 的動(dòng)作。flush 也會(huì)發(fā)生在關(guān)閉 handle 的時(shí)候。這代表當(dāng)我們碰到換行字元的時(shí)候,讀或?qū)懙膭?dòng)作都會(huì)停止并回報(bào)手邊的資料。但我們能使用 hFlush 來強(qiáng)迫回報(bào)所有已經(jīng)在 buffer 中的資料。經(jīng)過 flushing 之后,資料也就能被其他程式看見。
把 block-buffering 的讀取想成這樣:你的馬桶會(huì)在水箱有一加侖的水的時(shí)候自動(dòng)沖水。所以你不斷灌水進(jìn)去直到一加侖,馬桶就會(huì)自動(dòng)沖水,在水里面的資料也就會(huì)被看到。但你也可以手動(dòng)地按下沖水鈕來沖水。他會(huì)讓現(xiàn)有的水被沖走。沖水這個(gè)動(dòng)作就