Real World Haskell 第七章 I/O
生活随笔
收集整理的這篇文章主要介紹了
Real World Haskell 第七章 I/O
小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,幫大家做個(gè)參考.
| 幾乎所有程序都是用來(lái)從外部世界收集數(shù)據(jù),處理數(shù)據(jù),并把處理結(jié)果返回給外部世界的。也就是說(shuō),輸入和輸出對(duì)于程序設(shè)計(jì)來(lái)說(shuō)相當(dāng)關(guān)鍵。 |
| Haskell的I/O系統(tǒng)很強(qiáng)大,表達(dá)能力很強(qiáng)也很容易使用,理解它的原理對(duì)于學(xué)習(xí)Haskell來(lái)說(shuō)非常重要。Haskell把純函數(shù)式代碼和那些會(huì)對(duì)外部世界產(chǎn)生影響的代碼嚴(yán)格區(qū)分了開來(lái)。也就是說(shuō)它把副作用完全隔離在了純函數(shù)式的代碼之外。這樣不僅可以幫助程序員更容易驗(yàn)證程序的正確性,也能讓編譯器自動(dòng)進(jìn)行優(yōu)化和并行化。 |
| 本章先從Haskell簡(jiǎn)單的標(biāo)準(zhǔn)I/O開始。然后我們?cè)賮?lái)討論一些其他更強(qiáng)大的做法,以及更詳細(xì)地探討I/O是如何與純粹、惰性、函數(shù)式的Haskell世界相融合的。 |
| Haskell中的經(jīng)典I/O |
| 我們先來(lái)看一個(gè)程序,它和其他語(yǔ)言如 C 或 Perl中操作I/O的方式非常像。 |
| -- file: ch07/basicio.hs |
| main = do |
| putStrLn "Greetings! What is your name?" |
| inpStr <- getLine |
| putStrLn $ "Welcome to Haskell, " ++ inpStr ++ "!" |
| 可以把這個(gè)程序編譯成獨(dú)立可執(zhí)行文件,或者用runghc執(zhí)行它,也可以在 ghci中調(diào)用它的main函數(shù)。這里是一個(gè)用 runghc 的例子: |
| $ runghc basicio.hs |
| Greetings! What is your name? |
| John |
| Welcome to Haskell, John! |
| 輸出的結(jié)果還是相當(dāng)明了的,可以看到 putStrLn 輸出了一個(gè) String 和一個(gè)換行符。getLine從標(biāo)準(zhǔn)輸入中讀入一行。你可能還不太了解 <- 的語(yǔ)法。簡(jiǎn)單地說(shuō),它就是把執(zhí)行I/O動(dòng)作的結(jié)果綁定到變量名上。然后我們用列表連接操作符 ++ 來(lái)把輸入的字符串和程序自己的文本連接起來(lái)。 |
| 我們來(lái)看一下 putStrLn 和 getLine 的類型,你可以從庫(kù)參考文檔中找到這個(gè)信息,或者直接問(wèn)ghci: |
| ghci> :type putStrLn |
| putStrLn :: String -> IO () |
| ghci> :type getLine |
| getLine :: IO String |
| 注意這兩個(gè)類型的返回值里都有IO類型。這樣我們就可以判斷出他們要么是具有副作用,要么是用相同參數(shù)調(diào)用時(shí)返回結(jié)果可能不同,也可能是二者皆有。putStrLn 的類型看上去像個(gè)函數(shù)。接受一個(gè)String類型的參數(shù)并返回一個(gè) IO () 類型的值。那么到底什么是 IO () 呢? |
| 類型為 IO 某某 的就是一個(gè)I/O動(dòng)作。你可以保存它們,但是不會(huì)產(chǎn)生任何影響。你可以寫一句 writefoo = putStrLn "foo",但這句話不會(huì)做任何有用的事。但是如果之后在另一個(gè) I/O 動(dòng)作中使用到了 writefoo,那么當(dāng)父動(dòng)作被調(diào)用的時(shí)候,writefoo就會(huì)被執(zhí)行 -- I/O動(dòng)作可以通過(guò)更大的 I/O動(dòng)作結(jié)合在一起。()是一個(gè)空的元組(發(fā)音為 "unit"),表示從 putStrLn中沒(méi)有返回值。與 Java或C中的void類似。 |
| [Tip] Tip |
| 動(dòng)作可以在任何地方進(jìn)行創(chuàng)建,賦值,和傳遞。但是,他們只有從另一個(gè)I/O動(dòng)作中才能被執(zhí)行。 |
| 讓我們?cè)趃hci中看一下: |
| ghci> let writefoo = putStrLn "foo" |
| ghci> writefoo |
| foo |
| 在這個(gè)例子里,foo被輸出并不是因?yàn)閜utStrLn返回了它。而是putStrLn的副作用導(dǎo)致foo被寫到終端。 |
| 還有一件事需要注意:ghci實(shí)際“執(zhí)行”了writefoo。這意味著,給ghci一個(gè)I/O動(dòng)作,它就會(huì)立即執(zhí)行它。 |
| [Note] I/O動(dòng)作是什么? |
| I/O動(dòng)作 (Action): |
| * 類型為 IO t |
| * 是 Haskell 中的一等公民,并與Haskell的類型系統(tǒng)無(wú)縫的結(jié)合 |
| * 執(zhí)行時(shí)產(chǎn)生副作用,但求值時(shí)不會(huì)。也就是說(shuō),只有在某個(gè) I/O環(huán)境下被調(diào)用時(shí)才會(huì)產(chǎn)生副作用。 |
| * 任何表達(dá)式都可以返回I/O動(dòng)作,但是這個(gè)I/O動(dòng)作只有在另一個(gè) I/O動(dòng)作(或者main)內(nèi)才會(huì)被執(zhí)行。 |
| * 執(zhí)行一個(gè)類型為 IO t 的動(dòng)作會(huì)執(zhí)行相應(yīng)的 I/O動(dòng)作,并返回一個(gè) t 類型的值。 |
| getLine 的類型看上去可能有些奇怪。看起來(lái)它更像一個(gè)值而不是函數(shù)。實(shí)際上,可以這么看它:getLine存儲(chǔ)了一個(gè)I/O動(dòng)作。當(dāng)該動(dòng)作被執(zhí)行時(shí),返回一個(gè)String。<-操作符用來(lái)把結(jié)果從被執(zhí)行的動(dòng)作中“拉出來(lái)”,并存儲(chǔ)到一個(gè)變量里。 |
| main本身是一個(gè)類型為 IO () 的動(dòng)作。你只能從另一個(gè) I/O動(dòng)作中執(zhí)行I/O動(dòng)作。所以Haskell程序中所有的 I/O動(dòng)作最終都是由main驅(qū)動(dòng)的,那是每個(gè)Haskell程序開始執(zhí)行的地方。這就是Haskell提供的隔離副作用的機(jī)制:在I/O動(dòng)作中執(zhí)行I/O動(dòng)作,并從那里調(diào)用純函數(shù)式的(非I/O)函數(shù)。大部分Haskell代碼是純的;I/O動(dòng)作執(zhí)行I/O的動(dòng)作同時(shí)調(diào)用這些純的代碼。 |
| 要執(zhí)行一組動(dòng)作, do是很方便的一個(gè)方法。后面會(huì)看到,還有其他方法。當(dāng)使用do時(shí),縮進(jìn)變得很重要;你需要要保證所有的動(dòng)作代碼都對(duì)齊了。 |
| 只有當(dāng)你需要執(zhí)行多于一個(gè)動(dòng)作的時(shí)候才需要使用 do,整個(gè)do程序塊的返回值就是最后一個(gè)被執(zhí)行的動(dòng)作的返回值。在“剖析do程序塊”一節(jié)有對(duì)do語(yǔ)法的完整解釋。 |
| 我們來(lái)看一個(gè)在I/O動(dòng)作中調(diào)用純函數(shù)式代碼的例子: |
| -- file: ch07/callingpure.hs |
| name2reply :: String -> String |
| name2reply name = |
| "Pleased to meet you, " ++ name ++ ".\n" ++ |
| "Your name contains " ++ charcount ++ " characters." |
| where charcount = show (length name) |
| main :: IO () |
| main = do |
| putStrLn "Greetings once again. What is your name?" |
| inpStr <- getLine |
| let outStr = name2reply inpStr |
| putStrLn outStr |
| 注意這個(gè)例子中的 name2reply 函數(shù)。它是一個(gè)普通的 Haskell 函數(shù),遵從我們已經(jīng)說(shuō)過(guò)的所有規(guī)則:當(dāng)給出相同輸入時(shí)總是返回相同的結(jié)果,沒(méi)有副作用,惰性求值。它用到了一些其他的Haskell函數(shù): (++), show, 和 length。 |
| 到后面的main中,我們把 name2reply inpStr 的值綁定到 outStr 變量。在do程序塊中時(shí),用 <- 獲得IO動(dòng)作的返回值,用 let 獲得純函數(shù)式代碼的返回值。在do程序塊中,你不能在let語(yǔ)句后面使用 in 。 |
| 在這段代碼中你可以看到如何從鍵盤上讀取一個(gè)人的名字。然后這個(gè)名字被傳遞給一個(gè)純函數(shù),然后其結(jié)果被輸出。實(shí)際上,main的最后兩行也可以用 putStrLn (name2reply inpStr) 來(lái)代替的。這樣盡管main 確實(shí)具有副作用(它讓終端上出現(xiàn)了一些字符),而name2reply 沒(méi)有并且不可能有副作用。因?yàn)閚ame2reply是純函數(shù),不是一個(gè)動(dòng)作。 |
| 我們可以ghci中驗(yàn)證一下: |
| ghci> :load callingpure.hs |
| [1 of 1] Compiling Main ( callingpure.hs, interpreted ) |
| Ok, modules loaded: Main. |
| ghci> name2reply "John" |
| "Pleased to meet you, John.\nYour name contains 4 characters." |
| ghci> putStrLn (name2reply "John") |
| Pleased to meet you, John. |
| Your name contains 4 characters. |
| 字符串中的\n 是換行符,讓終端在輸出時(shí)開始一個(gè)新行。在ghci中直接調(diào)用 name2reply "John"的話會(huì)在字面上顯示 \n ,因?yàn)樗怯胹how來(lái)顯示函數(shù)返回值的。但是使用putStrLn 的話,會(huì)把字符串發(fā)送給終端,終端則會(huì)把 \n 翻譯成換行符。 |
| 你覺(jué)得要是直接在ghci中輸入main 會(huì)發(fā)生什么呢?你可以自己試一下。 |
| 看過(guò)了這些例子程序之后,你可能會(huì)想Haskell其實(shí)是命令式的而不是純粹的、惰性的,函數(shù)式的。這些例子有些看上去好像就是一系列動(dòng)作按順序執(zhí)行。但是還里面確實(shí)還有更深刻的含義。我們將在本章后面部分的"Haskell真的是命令式的么?"和"惰性I/O"兩節(jié)中繼續(xù)探討這個(gè)問(wèn)題。 |
| Pure vs. I/O |
| 為了幫助理解純函數(shù)式代碼和I/O之間究竟有何不同,這里給出一個(gè)對(duì)照表。當(dāng)我們說(shuō)純函數(shù)式代碼時(shí),我們說(shuō)的是那些對(duì)相同輸入總是返回相同結(jié)果,并且沒(méi)有副作用的Haskell函數(shù)。在Haskell里,只有I/O動(dòng)作的執(zhí)行不適用于這些規(guī)則。 |
| Table 7.1. Pure vs. Impure |
| Pure Impure |
| 純 非純 |
| 給定相同參數(shù)總是返回相同值 對(duì)相同參數(shù)可能返回不通值 |
| 永遠(yuǎn)沒(méi)有副作用 可以有副作用 |
| 用于不改變狀態(tài) 可以改變程序,系統(tǒng)或外界的全局狀態(tài) |
| 為什么純粹性如此重要 |
| 這一節(jié)中我們已經(jīng)探討了Haskell是如何將純函數(shù)式代碼與 I/O動(dòng)作清楚的分離開來(lái)的。大多數(shù)語(yǔ)言并不會(huì)這樣區(qū)分。像 C 或 Java這樣的語(yǔ)言里,編譯器不能保證某一個(gè)函數(shù)對(duì)相同的參數(shù)總是返回相同的值,或者保證一個(gè)函數(shù)永遠(yuǎn)沒(méi)有副作用。要想知道一個(gè)函數(shù)是否有副作用,唯一的辦法就是去讀它的文檔,而這文檔還不一定準(zhǔn)確。 |
| 程序中很多bug都是由一些出乎意料的副作用導(dǎo)致的。還有一些就是因?yàn)楸灰粋€(gè)函數(shù)對(duì)相同的輸入返回不同的結(jié)果給搞糊涂了。隨著多線程和其他形式的并行變得越來(lái)越平常,要管理全局的副作用就變得愈加困難了。 |
| Haskell這種把副作用隔離進(jìn)I/O動(dòng)作的方法提供了一個(gè)清楚的邊界。你總是可以清楚地知道系統(tǒng)的哪一部分可能會(huì)修改狀態(tài),哪些不會(huì)。你總是可以確信程序中純函數(shù)式的那部分代碼不會(huì)產(chǎn)生出人意料的結(jié)果。這可以幫助你編寫程序。同樣也可以幫助編譯器來(lái)理解你的程序。例如最近一些版本的ghc就可以對(duì)代碼中純函數(shù)式的部分——這部分代碼可說(shuō)是計(jì)算中的圣杯——提供一定程度的自動(dòng)的并行處理。 |
| 關(guān)于這個(gè)主題的更多討論,見(jiàn) “惰性I/O的副作用”一節(jié)。 |
| 操作文件和句柄 |
| 現(xiàn)在你已經(jīng)看過(guò)如何通過(guò)計(jì)算機(jī)終端與用戶進(jìn)行交互了。當(dāng)然,你經(jīng)常會(huì)需要操作一些特定的文件。這個(gè),同樣很容易做到。 |
| Haskell為I/O定義了很多基本函數(shù),他們中的許多都與其他編程語(yǔ)言類似。System.IO 的庫(kù)參考文檔提供了所有基本I/O函數(shù)的概述,如果你需要某個(gè)在本文中沒(méi)有涉及到的函數(shù),可以到參考哪里。 |
| 操作文件,一般從 openFile 開始,它會(huì)返回給你一個(gè)文件句柄。然后你就可以用這個(gè)句柄對(duì)那個(gè)文件進(jìn)行操作。Haskell提供了諸如hPutStrLn 這樣的函數(shù),它類似putStrLn,不過(guò)需要多傳一個(gè)文件句柄參數(shù),指定要操作的文件。操作完用 hClose來(lái)關(guān)閉句柄。這些函數(shù)都定義在 System.IO 中,因此在操作文件之前要先導(dǎo)入這個(gè)模塊。差不多所有非"h"開頭的函數(shù)都有與之相對(duì)的 "h"開頭的函數(shù);例如有一個(gè) print 用來(lái)向屏幕輸出,就有一個(gè)hPrint用來(lái)向文件輸出。 |
| 我們先來(lái)用命令式的方式來(lái)對(duì)文件進(jìn)行讀寫,應(yīng)該和其他語(yǔ)言里面的while循環(huán)有些類似。不過(guò)這并不是Haskell里最好的寫法;后面你還會(huì)看到更多更Haskell的做法。 |
| -- file: ch07/toupper-imp.hs |
| import System.IO |
| import Data.Char(toUpper) |
| main :: IO () |
| main = do |
| inh <- openFile "input.txt" ReadMode |
| outh <- openFile "output.txt" WriteMode |
| mainloop inh outh |
| hClose inh |
| hClose outh |
| mainloop :: Handle -> Handle -> IO () |
| mainloop inh outh = |
| do ineof <- hIsEOF inh |
| if ineof |
| then return () |
| else do inpStr <- hGetLine inh |
| hPutStrLn outh (map toUpper inpStr) |
| mainloop inh outh |
| 所有Haskell程序都是從main開始執(zhí)行。首先打開兩個(gè)文件:input.txt 以讀模式打開,output.txt 以寫模式打開。之后調(diào)用mainloop對(duì)文件進(jìn)行處理。 |
| mainloop首先檢查是否已經(jīng)到達(dá)文件的末尾(EOF)。如果不是就從輸入中讀入一行。把它轉(zhuǎn)換成大寫后寫入到輸出文件。之后遞歸的調(diào)用mainloop繼續(xù)處理文件。 |
| 注意這里對(duì)return的調(diào)用。這與C或Python中的return不一樣。在那些語(yǔ)言里,return用來(lái)立即中止當(dāng)前函數(shù)的執(zhí)行,并把值返回給調(diào)用者。在Haskell里,return與 <- 恰好相反。也就是說(shuō)return把一個(gè)純的值包裝成一個(gè)IO類型。因?yàn)槊恳粋€(gè)I/O動(dòng)作必須返回IO類型,如果你的結(jié)果來(lái)自純的計(jì)算,必須把它包裝成IO類型再返回。比如說(shuō),對(duì)于7這個(gè)Int,return 7就會(huì)創(chuàng)建一個(gè)類型為 IO Int的動(dòng)作。當(dāng)該動(dòng)作被執(zhí)行的時(shí)候,這個(gè)動(dòng)作會(huì)返回7.關(guān)于return更詳細(xì)探討,請(qǐng)看“return的本質(zhì)”一節(jié)。 |
| 讓我們嘗試運(yùn)行一下這個(gè)程序。假設(shè)我們已經(jīng)有了一個(gè) input.txt 文件,內(nèi)容如下: |
| This is ch08/input.txt |
| Test Input |
| I like Haskell |
| Haskell is great |
| I/O is fun |
| 123456789 |
| 執(zhí)行 runghc toupper-imp.hs ,之后會(huì)在目錄中找到 output.txt 文件。其內(nèi)容如下: |
| THIS IS CH08/INPUT.TXT |
| TEST INPUT |
| I LIKE HASKELL |
| HASKELL IS GREAT |
| I/O IS FUN |
| 123456789 |
| openFile詳解 |
| 讓我們用ghci來(lái)檢查下 openFile的類型: |
| ghci> :module System.IO |
| ghci> :type openFile |
| openFile :: FilePath -> IOMode -> IO Handle |
| FilePath 只是String的一個(gè)別名。在I/O函數(shù)中使用它而非String是為了指明該參數(shù)是特別用來(lái)作文件名用的,而不是一個(gè)常規(guī)的數(shù)據(jù)。 |
| IOMode指定文件如何管理。IOMode可能的取值列在表 7-2 中。 |
| IOMode的取值 |
| IOMode Can read? Can write? Starting position Notes |
| IOMode 可讀? 可寫? 開始位置 附注 |
| ReadMode Yes No 文件開頭 文件必須已經(jīng)存在 |
| WriteMode No Yes 文件開頭 文件如果已經(jīng)存在將會(huì)完全清空 |
| ReadWriteMode Yes Yes 文件開頭 如果文件不存在將會(huì)創(chuàng)建;否則已經(jīng)存在的數(shù)據(jù)不會(huì)動(dòng) |
| AppendMode No Yes 文件結(jié)尾 文件如果不存在將會(huì)創(chuàng)建;否則已經(jīng)存在的數(shù)據(jù)不會(huì)動(dòng) |
| 雖然本章大部分例子處理文本文件,但是Haskell也是可以處理二進(jìn)制文件的。如果要處理二進(jìn)制文件,就要用 openBinaryFile 代替 openFile。把文件當(dāng)作二進(jìn)制打開與作為文本打開,在Windows上處理時(shí)會(huì)有所不同。在Linux一類的操作系統(tǒng)上, openFile 和 openBinaryFile執(zhí)行的是完全相同的操作。不管怎樣,即使出于移植性的考慮,處理二進(jìn)制文件時(shí)也應(yīng)該總是使用openBinaryFile。 |
| 關(guān)閉句柄 |
| 你已經(jīng)看到hClose是用來(lái)關(guān)閉文件句柄的。讓我們花點(diǎn)時(shí)間來(lái)探討下為什么關(guān)閉句柄很重要。 |
| 在“緩沖”一節(jié)你將會(huì)看到,Haskell為文件維護(hù)了內(nèi)部的緩沖區(qū)。這帶來(lái)了很關(guān)鍵的性能提升。但是,這樣一來(lái)的話,以寫入模式打開的文件,可能要到調(diào)用hClose的時(shí)候,有些數(shù)據(jù)才會(huì)真正被寫到操作系統(tǒng)上去。 |
| 要確保對(duì)打開的文件調(diào)用hClose的另一個(gè)原因是它會(huì)占用系統(tǒng)資源。如果你的程序執(zhí)行很長(zhǎng)時(shí)間,并且打開了很多文件但是沒(méi)有關(guān)閉他們,你的程序很可能因?yàn)橘Y源耗盡而崩潰。這一點(diǎn)上Haskell與其他語(yǔ)言沒(méi)什么區(qū)別。 |
| 當(dāng)程序退出時(shí),Haskell一般會(huì)把仍然打開著的文件關(guān)閉。然而在某些情況下卻不一定,因此再次提醒大家,作為一個(gè)負(fù)責(zé)人的程序員,永遠(yuǎn)不能忘記調(diào)用hClose。 |
| Haskell還提供了一些工具,可以幫助你不論是否有錯(cuò)誤發(fā)生都能輕松確保打開的文件被關(guān)閉。你可以在“擴(kuò)展實(shí)例:函數(shù)式I/O和臨時(shí)文件”一節(jié)了解關(guān)于finally,在“獲取使用釋放循環(huán)”一節(jié)了解bracket。 |
| Seek 和 Tell |
| 當(dāng)通過(guò)句柄從磁盤讀寫文件的時(shí)候,操作系統(tǒng)內(nèi)部會(huì)記錄文件當(dāng)前操作所在的位置。每次讀取,操作系統(tǒng)會(huì)返回從當(dāng)前位置開始的一塊數(shù)據(jù),并根據(jù)讀取的數(shù)據(jù)將位置相應(yīng)地遞增。 |
| 可以用hTell獲得文件當(dāng)前的位置。當(dāng)文件剛剛創(chuàng)建時(shí),它是空的,位置為0。寫入了5個(gè)字節(jié)后,它的位置變?yōu)?,等等。hTell取一個(gè)句柄做參數(shù),返回一個(gè) IO Integer 表示位置。 |
| 與hTell相伴的是hSeek。hSeek可以讓你修改文件的位置。它接受三個(gè)參數(shù):文件句柄,偏移模式(SeekMode)和偏移量。 |
| 偏移模式(SeekMode)有三種,用來(lái)表示如何對(duì)給出的偏移量進(jìn)行解釋。AbsoluteSeek 意思是給定的偏移是文件中的精確位置,這與hTell給出的信息是一致的。RelativeSeek 意思是以當(dāng)前位置為原點(diǎn)進(jìn)行偏移,一個(gè)正數(shù)的偏移量表示向前偏移,而負(fù)數(shù)表示向后偏移。最后SeekFromEnd將從文件末尾向前偏移指定數(shù)量的字節(jié)。 hSeek handle SeekFromEnd 0 將把你帶到文件的末尾。“擴(kuò)展實(shí)例:函數(shù)式I/O和臨時(shí)文件”一節(jié)有一個(gè)hSeek的例子。 |
| 并不是所有的句柄都是可以進(jìn)行偏移的。一般來(lái)說(shuō)句柄都是指向文件,但是它也能夠指向其他一些不能進(jìn)行偏移操作的東西,例如網(wǎng)絡(luò)連接,磁帶驅(qū)動(dòng)器,或者終端。可以用hIsSeekable 來(lái)檢查一個(gè)給定的句柄是否支持偏移。 |
| 標(biāo)準(zhǔn)輸入,標(biāo)準(zhǔn)輸出,標(biāo)準(zhǔn)錯(cuò)誤 |
| 之前我們提到過(guò)每一個(gè)非"h"的函數(shù),一般都有一個(gè) "h"函數(shù)與之對(duì)應(yīng),可以用來(lái)處理任何句柄。實(shí)際上,非"h"的函數(shù)只不過(guò)是他們的"h"函數(shù)的一種快捷方式而已。 |
| 在System.IO中有三個(gè)預(yù)定義的句柄。這些句柄總是可用的。它們就是標(biāo)準(zhǔn)輸入 stdin ;標(biāo)準(zhǔn)輸出 stdout; 和標(biāo)準(zhǔn)錯(cuò)誤 stderr。標(biāo)準(zhǔn)輸入一般指鍵盤,標(biāo)準(zhǔn)輸出指顯示屏,標(biāo)準(zhǔn)錯(cuò)誤一般也指向顯示屏。 |
| 我們可以這樣來(lái)定義getLine一類的函數(shù): |
| getLine = hGetLine stdin |
| putStrLn = hPutStrLn stdout |
| print = hPrint stdout |
| [Tip] Tip |
| 這里使用了部分函數(shù)。如果不清楚,回顧下“部分函數(shù)應(yīng)用和柯里化”一節(jié)。 |
| 剛才我們對(duì)三個(gè)標(biāo)準(zhǔn)文件句柄的解釋的只是它們“通常”都指向什么,有的操作系統(tǒng)還允許你在啟動(dòng)的時(shí)候把這些文件句柄重定向到其他的地方——文件,設(shè)備,甚至是其他程序。這個(gè)特性在POSIX系統(tǒng)(Linux,BSD, Mac)上的shell腳本中被廣泛應(yīng)用,在Windows上也可以使用。 |
| 一般來(lái)說(shuō)使用標(biāo)準(zhǔn)輸入輸出而非顯示指定文件是有好處的,這樣你可以通過(guò)終端和用戶進(jìn)行交互。同時(shí)也允許你操作輸入輸出文件,如果需要的話甚至還可以和其他程序組合在一起。 |
| 舉個(gè)例子來(lái)說(shuō),你可以這種方式給 callingpure.hs 提供輸入: |
| $ echo John|runghc callingpure.hs |
| Greetings once again. What is your name? |
| Pleased to meet you, John. |
| Your name contains 4 characters. |
| 當(dāng)執(zhí)行 callingpure.hs 時(shí),它不需要等待鍵盤輸入,而是從 echo 程序接收到 John。同時(shí)注意到和用鍵盤輸入時(shí)不同,輸出中沒(méi)有John的那一行。終端把你鍵入的內(nèi)容回顯給你,但這是通過(guò)另一個(gè)程序進(jìn)行輸入,不會(huì)把輸入包含在輸出流里。 |
| 刪除和重命名文件 |
| 本章前面部分探討了如何操作文件的內(nèi)容。現(xiàn)在讓我們來(lái)關(guān)心下如何操作文件本身。 |
| System.Directory 模塊里有兩個(gè)函數(shù)還是挺有用的。一個(gè)是removeFile,它只接受一個(gè)文件名作為參數(shù),執(zhí)行的操作就是刪除這個(gè)文件。renameFile取兩個(gè)文件名作參數(shù):第一個(gè)是舊的文件名,第二個(gè)是新文件名。如果兩個(gè)文件名處于不同的目錄中,你也把他當(dāng)成是移動(dòng)操作。調(diào)用renameFile前舊文件名必須已經(jīng)存在。如果新的文件名已經(jīng)存在了,會(huì)先把它刪了,然后再進(jìn)行改名操作。 |
| 像其他取文件名做參數(shù)的函數(shù)一樣,renameFile在舊文件名不存在的情況下會(huì)拋出異常。第19章《錯(cuò)誤處理》將會(huì)介紹更多異常處理的信息。 |
| System.Directory模塊中還有很多函數(shù)用來(lái)進(jìn)行目錄的創(chuàng)建和刪除,獲取目錄中的文件列表,檢測(cè)文件是否存在。在“目錄和文件信息”一節(jié)將會(huì)對(duì)這些話題進(jìn)行探討。 |
| 臨時(shí)文件 |
| 程序員經(jīng)常需要?jiǎng)?chuàng)建臨時(shí)文件。這些文件可以用來(lái)存儲(chǔ)計(jì)算需要的大量數(shù)據(jù),或者是供給其他程序或其他用戶使用的數(shù)據(jù)。 |
| 你可以手動(dòng)為創(chuàng)建的文件取一個(gè)獨(dú)一無(wú)二的文件名,但是在不同的平臺(tái)上安全地做到這一點(diǎn)還是需要處理一些細(xì)節(jié)上的不同。Haskell提供了一個(gè)方便的函數(shù)叫 openTempFile (和對(duì)應(yīng)的openBinaryTempFile),可以幫你處理這個(gè)問(wèn)題。 |
| openTempFile 需要兩個(gè)參數(shù):要?jiǎng)?chuàng)建文件的目錄和文件名命名的“模板”。目錄可以直接用 "."表示當(dāng)前工作目錄。或者使用System.Directory.getTemporaryDirectory得到機(jī)器上的臨時(shí)目錄。文件名模板作為創(chuàng)建文件名的基礎(chǔ),然后再添加一些隨機(jī)字符上去,以確保產(chǎn)生的文件名是真正獨(dú)一無(wú)二的。 |
| openTempFile 的返回類型是 IO (FilePath, Handle)。元組的第一部分是創(chuàng)建文件的文件名,第二部分是以 ReadWriteMode 模式打開的文件句柄。當(dāng)操作完文件句柄后,要用 hClose 將它關(guān)閉,并調(diào)用 removeFile 刪除該臨時(shí)文件。下面舉個(gè)例子: |
| 擴(kuò)展實(shí)例:函數(shù)式I/O和臨時(shí)文件 |
| 這里有一個(gè)比較龐大的例子,它融合了本章以及之前章節(jié),甚至一些還沒(méi)見(jiàn)過(guò)的概念。嘗試閱讀本程序,看看能否看出它是做什么的,以及是如何去做的。 |
| -- file: ch07/tempfile.hs |
| import System.IO |
| import System.Directory(getTemporaryDirectory, removeFile) |
| import System.IO.Error(catch) |
| import Control.Exception(finally) |
| -- 主程序入口。在myAction中使用臨時(shí)文件 |
| main :: IO () |
| main = withTempFile "mytemp.txt" myAction |
| {- |
| 程序核心部分。傳遞一個(gè)文件路徑和一個(gè)臨時(shí)文件句柄進(jìn)行調(diào)用。 |
| myAction 是從 withTempFile 中調(diào)用的,所以當(dāng) myAction 函數(shù)退出時(shí),臨時(shí)文件將會(huì)被自動(dòng)關(guān)閉并刪除,。 |
| -} |
| myAction :: FilePath -> Handle -> IO () |
| myAction tempname temph = |
| do -- 在終端上顯示歡迎詞 |
| putStrLn "Welcome to tempfile.hs" |
| putStrLn $ "I have a temporary file at " ++ tempname |
| -- 查看下初始位置 |
| pos <- hTell temph |
| putStrLn $ "My initial position is " ++ show pos |
| -- 向臨時(shí)文件中寫入一些數(shù)據(jù) |
| let tempdata = show [1..10] |
| putStrLn $ "Writing one line containing " ++ |
| show (length tempdata) ++ " bytes: " ++ |
| tempdata |
| hPutStrLn temph tempdata |
| -- 查看新的位置,實(shí)際上這并不改變pos在內(nèi)存中的值, |
| -- 但是它讓 "pos" 變量在 "do" 程序塊后面的部分中指向了另一個(gè)值。 |
| pos <- hTell temph |
| putStrLn $ "After writing, my new position is " ++ show pos |
| -- 轉(zhuǎn)移到文件起始位置并開始顯示 |
| putStrLn $ "The file content is: " |
| hSeek temph AbsoluteSeek 0 |
| -- hGetContents 惰性讀取整個(gè)文件 |
| c <- hGetContents temph |
| -- 把文件一個(gè)字節(jié)一個(gè)字節(jié)輸出,后跟 \n |
| putStrLn c |
| -- 以 Haskell 字面量形式顯示 |
| putStrLn $ "Which could be expressed as this Haskell literal:" |
| print c |
| {- |
| 本函數(shù)接收兩個(gè)參數(shù):臨時(shí)文件名模式和一個(gè)函數(shù)。它會(huì)創(chuàng)建一個(gè)臨時(shí)文件,并將文件名和文件句柄傳遞給那個(gè)函數(shù)。 |
| 臨時(shí)文件用通過(guò) openTempFile 創(chuàng)建的。目錄是 getTemporaryDirectory 所指定的,如果系統(tǒng)沒(méi)有臨時(shí)目錄概念,則用 "." 。 |
| 給定的文件名模式被傳遞給了 openTempFile。 |
| 當(dāng)給定函數(shù)中止,即使是異常中止,文件句柄也會(huì)被關(guān)閉,同時(shí)臨時(shí)文件被刪除。 |
| -} |
| withTempFile :: String -> (FilePath -> Handle -> IO a) -> IO a |
| withTempFile pattern func = |
| do -- The library ref says that getTemporaryDirectory may raise on |
| -- exception on systems that have no notion of a temporary directory. |
| -- So, we run getTemporaryDirectory under catch. catch takes |
| -- two functions: one to run, and a different one to run if the |
| -- first raised an exception. If getTemporaryDirectory raised an |
| -- exception, just use "." (the current working directory). |
| 庫(kù)參考手冊(cè)里說(shuō)如果系統(tǒng)不支持臨時(shí)目錄概念的話,getTemporaryDirectory 將會(huì)拋出異常。 |
| 因此我們?cè)?catch 之下來(lái)執(zhí)行 getTemporaryDirectory。catch 接受兩個(gè)函數(shù)參數(shù):一個(gè)會(huì)直接運(yùn)行,另一個(gè)會(huì)在第一個(gè)函數(shù)拋出異常時(shí)運(yùn)行。如果getTemporaryDirectory拋出了異常,就使用"." |
| tempdir <- catch (getTemporaryDirectory) (\_ -> return ".") |
| (tempfile, temph) <- openTempFile tempdir pattern |
| -- 調(diào)用 (func tempfile temph) 對(duì)臨時(shí)文件進(jìn)行操作。 finally 需要兩個(gè)操作作為參數(shù)。第一個(gè)會(huì)被直接執(zhí)行。 |
| -- 第一個(gè)操作執(zhí)行之后,不論是否拋出異常,第二個(gè)操作都會(huì)被執(zhí)行。 |
| -- 這樣,我們就能確保臨時(shí)文件總是會(huì)被刪除。finally 返回第一個(gè)操作的返回值。 |
| finally (func tempfile temph) |
| (do hClose temph |
| removeFile tempfile) |
| 我們來(lái)看下程序的結(jié)尾。從withTempFile這個(gè)函數(shù)可以看出Haskell在引入I/O的時(shí)候并沒(méi)有忘記它自己函數(shù)式的本質(zhì)。這個(gè)函數(shù)取一個(gè)String和另一個(gè)函數(shù)做參數(shù)。withTempFile使用臨時(shí)文件的文件名和句柄傳入的函數(shù)進(jìn)行調(diào)用。當(dāng)傳入的函數(shù)退出時(shí),臨時(shí)文件被關(guān)閉和刪除。因此即使在處理I/O時(shí),我們依然能夠看到傳遞函數(shù)作為參數(shù)的用法。Lisp程序員大概會(huì)發(fā)現(xiàn)這個(gè) withTempFile 函數(shù)跟Lisp里的 with-open-file 函數(shù)很類似。 |
| 適當(dāng)?shù)漠惓L幚砜梢宰屇愕某绦蛟谟龅藉e(cuò)誤時(shí)更加健壯。通常情況下對(duì)臨時(shí)處理完成后都需要把臨時(shí)文件刪除,即使發(fā)生了錯(cuò)誤也不例外。我們需要保證這一點(diǎn)。異常處理更多信息見(jiàn)第19章《錯(cuò)誤處理》。 |
| 回到程序的開始,main的定義只是簡(jiǎn)單的 withTempFile "mytemp.txt" myAction 。 之后使用臨時(shí)文件的文件名和文件句柄調(diào)用 myAction。 |
| 然后myAction在終端上顯示一些信息,向文件中寫入一些數(shù)據(jù),再移動(dòng)到文件開頭用 hGetContents 讀取數(shù)據(jù),再把文件內(nèi)容一個(gè)字節(jié)一個(gè)字節(jié)的顯示出來(lái)。之后又通過(guò) print c 來(lái)按Haskell的字面量格式輸出出來(lái),最后這個(gè)操作等價(jià)于 putStrLn (show c) 相同。 |
| 讓我們來(lái)看看輸出: |
| $ runhaskell tempfile.hs |
| Welcome to tempfile.hs |
| I have a temporary file at /tmp/mytemp8572.txt |
| My initial position is 0 |
| Writing one line containing 22 bytes: [1,2,3,4,5,6,7,8,9,10] |
| After writing, my new position is 23 |
| The file content is: |
| [1,2,3,4,5,6,7,8,9,10] |
| Which could be expressed as this Haskell literal: |
| "[1,2,3,4,5,6,7,8,9,10]\n" |
| 每次運(yùn)行此程序,臨時(shí)文件名都會(huì)稍有不同,因?yàn)樗须S機(jī)生成的部分。觀察這些輸出,你可能會(huì)有如下一些疑問(wèn): |
| 1、為什么寫入了22個(gè)字節(jié)后,位置是23? |
| 2、為什么文件內(nèi)容最后顯示有一個(gè)空行? |
| 3、為什么在Haskell字面格式顯示的末尾有一個(gè) \n |
| 你可能已經(jīng)猜到這三個(gè)問(wèn)題的互相之間都是有關(guān)聯(lián)的。你可以先自己想一會(huì),看能否找出答案。如果需要幫助,這里也為你提供一些解釋: |
| 1、因?yàn)槲覀兪怯玫?hPutStrLn 而不是 hPutStr 來(lái)寫入數(shù)據(jù)。hPutStrLn 總是會(huì)在行末尾添加 \n,這個(gè) \n 在 tempdate 中并不存在。 |
| 2、這里是用 putStrLn c 來(lái)顯示文件內(nèi)容 c 。因?yàn)槲募?nèi)容本來(lái)就是通過(guò) hPutStrLn 寫入的,所以 c 的末尾是個(gè)換行符,而 putStrLn 在輸出的時(shí)候又在末尾添加了一個(gè)換行符,結(jié)果就顯示出了一個(gè)空行。 |
| 3、\n 就是一開始hPutStrLn輸出的換行符。 |
| 最后說(shuō)明下,在不同的操作系統(tǒng)上字節(jié)數(shù)計(jì)算方式會(huì)有所不同。例如在Windows上使用\r\n的雙子節(jié)序列作為行尾標(biāo)記,因此在不同的平臺(tái)上可能看上去會(huì)不太一樣。 |
| 惰性I/O |
| 本章到目前為止,你看到的例子都是比較傳統(tǒng)的 I/O 處理方式。每一行或每一塊數(shù)據(jù)都是單獨(dú)讀取的,并且也需要單獨(dú)進(jìn)行處理。 |
| Haskell還提供另一種方式。因?yàn)镠askell是惰性語(yǔ)言,也就是說(shuō)只有到最后關(guān)頭才會(huì)對(duì)數(shù)據(jù)的值進(jìn)行求值,所以這里會(huì)一些新穎的處理I/O的方法。 |
| hGetContents |
| 新方法之一就是hGetContents函數(shù)。hGetContents 的類型是 Handle -> IO String。返回的 String 的值就是指定文件句柄中的全部?jī)?nèi)容。 |
| 在嚴(yán)格求值的語(yǔ)言里,使用這樣的函數(shù)往往是不太好的。因?yàn)樽x取一個(gè)2KB的文件還可以,要是讀取一個(gè)500GB文件的全部?jī)?nèi)容,將很可能會(huì)因?yàn)閮?nèi)存不夠而崩潰。在這些語(yǔ)言中,需要用傳統(tǒng)的機(jī)制如循環(huán)來(lái)處理文件的全部?jī)?nèi)容。 |
| 但是hGetContents是不一樣的。它返回的String是惰性求值的。也就是說(shuō)在調(diào)用hGetContents 時(shí)并不會(huì)進(jìn)行實(shí)際的讀取操作。只有當(dāng)列表的元素(字符)被處理時(shí)才會(huì)從文件句柄中讀取數(shù)據(jù)。當(dāng)String中的元素不被使用時(shí),Haskell的垃圾收集器會(huì)自動(dòng)釋放它的內(nèi)存。所有這些都是透明的。它看上去就像是——其實(shí)實(shí)際上也是——一個(gè)純的String,因此你可以把它傳遞給純函數(shù)式的代碼(沒(méi)有IO)。 |
| 我們來(lái)看一個(gè)例子。前一節(jié)“處理文件和句柄”中有一個(gè)命令式的程序,把文件的全部?jī)?nèi)容轉(zhuǎn)換成大寫。它的命令式算法與其他很多語(yǔ)言中見(jiàn)到的差不多。這里展示一個(gè)利用惰性求值的簡(jiǎn)單地多的算法: |
| -- file: ch07/toupper-lazy1.hs |
| import System.IO |
| import Data.Char(toUpper) |
| main :: IO () |
| main = do |
| inh <- openFile "input.txt" ReadMode |
| outh <- openFile "output.txt" WriteMode |
| inpStr <- hGetContents inh |
| let result = processData inpStr |
| hPutStr outh result |
| hClose inh |
| hClose outh |
| processData :: String -> String |
| processData = map toUpper |
| 注意hGetContents為我們處理了所有的讀取邏輯。還有 processData 這個(gè)函數(shù),它是一個(gè)純函數(shù),因?yàn)樗鼪](méi)有副作用,并且使用相同的參數(shù)調(diào)用總是返回相同的結(jié)果。它不需要——也沒(méi)辦法——知道它的輸入是從文件中惰性讀取的。不管是20個(gè)字符的字符串還是磁盤上存的500GB的數(shù)據(jù),它都可以完美地進(jìn)行處理。 |
| 可以使用ghci來(lái)確認(rèn)一下: |
| ghci> :load toupper-lazy1.hs |
| [1 of 1] Compiling Main ( toupper-lazy1.hs, interpreted ) |
| Ok, modules loaded: Main. |
| ghci> processData "Hello, there! How are you?" |
| "HELLO, THERE! HOW ARE YOU?" |
| ghci> :type processData |
| processData :: String -> String |
| ghci> :type processData "Hello!" |
| processData "Hello!" :: String |
| [Warning] Warning |
| 在上面的例子中,如果你想在處理完inpStr變量之后保留著繼續(xù)使用的話,程序的內(nèi)存利用率會(huì)有所降低。這是因?yàn)榫幾g器必須把inpStr 的值保留在內(nèi)存中,后面才可以繼續(xù)使用。上面這個(gè)程序里,編譯器知道 inpStr 永遠(yuǎn)不會(huì)再用了,因此會(huì)立刻把它釋放掉。只要記住:內(nèi)存只有在最后一次使用后才會(huì)被釋放。 |
| 為了清楚得展示其中純函數(shù)式的代碼的使用,使得這個(gè)程序?qū)懙糜悬c(diǎn)冗長(zhǎng),這里有個(gè)更簡(jiǎn)潔的版本,接下來(lái)將以這個(gè)程序作為基礎(chǔ)。 |
| -- file: ch07/toupper-lazy2.hs |
| import System.IO |
| import Data.Char(toUpper) |
| main = do |
| inh <- openFile "input.txt" ReadMode |
| outh <- openFile "output.txt" WriteMode |
| inpStr <- hGetContents inh |
| hPutStr outh (map toUpper inpStr) |
| hClose inh |
| hClose outh |
| 使用hGetContents時(shí)并不一定要處理輸入文件的全部?jī)?nèi)容。當(dāng)Haskell系統(tǒng)斷定hGetContents返回的整個(gè)字符串可以被垃圾回收時(shí)——意味著它再也不會(huì)被用到——它會(huì)自動(dòng)關(guān)閉相應(yīng)文件。從文件讀出來(lái)的數(shù)據(jù)也是一樣的處理。當(dāng)一段數(shù)據(jù)不再需要的時(shí)候,Haskell環(huán)境將會(huì)釋放占有的內(nèi)存。嚴(yán)格地說(shuō)在這個(gè)例子里根本不需要調(diào)用hClose。不過(guò)加上它仍然是一個(gè)好習(xí)慣,因?yàn)閷?lái)對(duì)程序的修改可能會(huì)使得調(diào)用hClose變得必要。 |
| [警告]警告 |
| 當(dāng)使用hGetContents時(shí),要注意一點(diǎn):就算后面的程序中并不直接引用這個(gè)文件句柄,你也必須等到 hGetContents 的結(jié)果被處理完后才能關(guān)閉這個(gè)句柄。如果提前關(guān)閉可能會(huì)導(dǎo)致文件數(shù)據(jù)的丟失。因?yàn)镠askell是惰性的,你通常可以做出這樣的假設(shè),你輸出了多少計(jì)算結(jié)果,就有多少計(jì)算涉及的輸入被讀取。 |
| readFile和writeFile |
| Haskell程序員經(jīng)常把hGetContents用作過(guò)濾器。他們從一個(gè)文件讀取數(shù)據(jù),對(duì)數(shù)據(jù)進(jìn)行一些處理,把結(jié)果寫出到其他地方。因?yàn)檫@個(gè)模式如此常見(jiàn),所以就產(chǎn)生了一些更快捷的方式。readFile 和 writeFile 就是這樣的快捷方式,他們可以把文件直接當(dāng)作字符串進(jìn)行處理。它們內(nèi)部會(huì)處理好打開文件,關(guān)閉文件,讀寫數(shù)據(jù)的所有細(xì)節(jié)問(wèn)題。readFile內(nèi)部就是使用hGetContents的。 |
| 你能猜出這些函數(shù)的類型么?我們用ghci來(lái)看看: |
| ghci> :type readFile |
| readFile :: FilePath -> IO String |
| ghci> :type writeFile |
| writeFile :: FilePath -> String -> IO () |
| 這里是一個(gè)使用readFile和writeFile的例子程序: |
| -- file: ch07/toupper-lazy3.hs |
| import Data.Char(toUpper) |
| main = do |
| inpStr <- readFile "input.txt" |
| writeFile "output.txt" (map toUpper inpStr) |
| 看,程序的核心部分只有兩行!readFile 返回一個(gè)惰性String,我們把它存儲(chǔ)在 inpStr里。然后處理它,并傳給 writeFile 寫到輸出文件。 |
| readFile和writeFile都沒(méi)有向你暴露文件句柄,因此也就不需要手動(dòng)調(diào)用 hClose。readFile內(nèi)部使用hGetContents,當(dāng)返回的String被垃圾回收,或者輸入文件被全部讀取完畢之后,隱藏文件句柄將會(huì)自動(dòng)被關(guān)閉。writeFile也會(huì)在輸入的String全部寫完后自動(dòng)關(guān)閉文件句柄。 |
| 關(guān)于惰性輸出 |
| 現(xiàn)在你應(yīng)該了解如何在Haskell中進(jìn)行惰性輸入了。但是惰性輸出又是怎么回事呢? |
| 我們已經(jīng)知道,在Haskell中,只有到真正被使用的時(shí)候才會(huì)對(duì)表達(dá)式進(jìn)行求值。像writeFile 和putStr這樣的函數(shù)把傳入的整個(gè)字符串寫到輸出文件,因此整個(gè)字符串都必須被求值。因此可以保證putStr的參數(shù)將會(huì)被完全求值。 |
| 但是這對(duì)惰性輸入又意味著什么呢?在上面的例子里,對(duì)putStr或writeFile的調(diào)用是否會(huì)立刻將整個(gè)字符串強(qiáng)制載入到內(nèi)存中? |
| 答案是否定的。 putStr(和所有類似的輸出函數(shù))負(fù)責(zé)在數(shù)據(jù)可用的時(shí)候把他們寫入到輸出文件,但它們沒(méi)有必要保留那些已經(jīng)被寫出的數(shù)據(jù),只要程序中沒(méi)有其他部分需要這些數(shù)據(jù),它們的內(nèi)存就會(huì)被立刻釋放。某種意義上來(lái)說(shuō),你可以把在readFile和writeFile 間傳遞的字符串想象成連接兩者的管道。數(shù)據(jù)從一端,經(jīng)過(guò)一些變形,流向另一端。 |
| 你可以給 toupper-lazy3.hs 生成一個(gè)大的 input.txt,來(lái)自己驗(yàn)證這一點(diǎn)。這會(huì)需要一點(diǎn)時(shí)間來(lái)處理,但是在它處理過(guò)程中你會(huì)看到一個(gè)很低且穩(wěn)定的內(nèi)存占用。 |
| interact |
| 我們已經(jīng)學(xué)到readFile 和 writeFile 是如何處理讀取文件,進(jìn)行轉(zhuǎn)換,再寫到另一個(gè)文件的這種常見(jiàn)情況。還有比這更常見(jiàn)的一種情況:從標(biāo)準(zhǔn)輸入讀入,進(jìn)行轉(zhuǎn)換,再將結(jié)果寫到標(biāo)準(zhǔn)輸出。要處理這種情況,有一個(gè)叫 interact 的函數(shù)。interact的類型是 (String -> String) -> IO ()。即取一個(gè)類型為 String -> String 的函數(shù)作為參數(shù)。它會(huì)通過(guò) getContents 惰性的讀入標(biāo)準(zhǔn)輸入,并將結(jié)果傳入這個(gè)函數(shù),再把這個(gè)函數(shù)的返回發(fā)送到標(biāo)準(zhǔn)輸出。 |
| 我們可以把我們上面的例程轉(zhuǎn)換成通過(guò)interact來(lái)操作標(biāo)準(zhǔn)輸入輸出,像下面這樣: |
| -- file: ch07/toupper-lazy4.hs |
| import Data.Char(toUpper) |
| main = interact (map toUpper) |
| 看,我們只用一行代碼就完成了轉(zhuǎn)換!要得到和之前例子相同的效果,需要像下面這樣來(lái)執(zhí)行: |
| $ runghc toupper-lazy4.hs < input.txt > output.txt |
| 或者,如果想在屏幕上看到輸出,可以這樣: |
| $ runghc toupper-lazy4.hs < input.txt |
| 如果你希望程序交互性地處理輸入輸出的話,執(zhí)行 runghc toupper-lazy4.hs 即可,不要帶其他命令行參數(shù)。可以看到每輸入一個(gè)字符,它都用大寫形式回顯出來(lái)。不過(guò)這種情況下不同緩沖模式可能會(huì)對(duì)程序的具體行為有所影響,關(guān)于緩沖的更多信息參見(jiàn)本章后面的“緩沖”一節(jié)。如果你遇到輸入一行結(jié)束時(shí)才回顯,或者暫時(shí)沒(méi)有進(jìn)行回顯,那就是緩沖惹得禍了。 |
| 我們也可以用 interact 來(lái)編寫一些簡(jiǎn)單的交互程序。我們先上一個(gè)簡(jiǎn)單的例子:把輸入轉(zhuǎn)換成大寫并在前面添加一行文本。 |
| -- file: ch07/toupper-lazy5.hs |
| import Data.Char(toUpper) |
| main = interact (map toUpper . (++) "Your data, in uppercase, is:\n\n") |
| [Tip] Tip |
| 如果對(duì) . 操作符的使用感到迷惑,不妨參考下“通過(guò)函數(shù)組合重用代碼”一節(jié)。 |
| 這里我們?cè)谳敵龅拈_頭添加了一個(gè)字符串。但是你可以看出這里面有什么問(wèn)題么? |
| 因?yàn)槲覀儗?duì) (++) 的結(jié)果調(diào)用 map,使得我們添加的這一串頭部字符串也變成大寫的了。修改一下: |
| -- file: ch07/toupper-lazy6.hs |
| import Data.Char(toUpper) |
| main = interact ((++) "Your data, in uppercase, is:\n\n" . |
| map toUpper) |
| 它把頭部移到了map 之外。 |
| 用 interact 做過(guò)濾器 |
| interact也常常用作過(guò)濾器。假設(shè)你要讀入一個(gè)文件,并將所有包含字母"a"的每一行打印出來(lái)。這里是一個(gè)使用 interact 的方法: |
| -- file: ch07/filter.hs |
| main = interact (unlines . filter (elem 'a') . lines) |
| 這里引出了三個(gè)還不熟悉的函數(shù)。我們?cè)趃hci中看下它們的類型: |
| ghci> :type lines |
| lines :: String -> [String] |
| ghci> :type unlines |
| unlines :: [String] -> String |
| ghci> :type elem |
| elem :: (Eq a) => a -> [a] -> Bool |
| 你可以通過(guò)它們的類型猜出這些函數(shù)是做什么的么?猜不到的話,也可以從“熱身:可移植的文本行切分”一節(jié)以及“特殊的字符串處理函數(shù)”一節(jié)中找到解釋。你會(huì)經(jīng)常看到在I/O動(dòng)作中使用 lines 和 unlines 函數(shù)。最后,elem 函數(shù)取一個(gè)元素和一個(gè)列表,如果這個(gè)元素出現(xiàn)在列表中的話就返回True。 |
| 用我們的標(biāo)準(zhǔn)輸入數(shù)據(jù)來(lái)運(yùn)行它: |
| $ runghc filter.hs < input.txt |
| I like Haskell |
| Haskell is great |
| 果然輸出了包含 "a" 字符的兩行。惰性過(guò)濾器是Haskell中強(qiáng)大的處理方式。可以想想看,一個(gè)過(guò)濾器——如標(biāo)準(zhǔn)Unix程序 grep——聽(tīng)上去就像一個(gè)函數(shù)。它獲取一些輸入,進(jìn)行一些計(jì)算,產(chǎn)生一些可預(yù)測(cè)的輸出。 |
| IO Monad |
| 前面我們已經(jīng)看了很多使用Haskell語(yǔ)言進(jìn)行I/O處理的例子,下面讓我們退一步,來(lái)考慮下I/O是如何與更廣泛的Haskell語(yǔ)言進(jìn)行融合的。 |
| 因?yàn)镠askell是一種純函數(shù)的語(yǔ)言,每次用相同的參數(shù)調(diào)用一個(gè)函數(shù)都會(huì)返回相同的結(jié)果。而且,函數(shù)也不會(huì)改變程序的全局狀態(tài)。 |
| 那您可能會(huì)好奇I/O是如何融合進(jìn)這樣一種情況的呢,因?yàn)楹茱@然,如果從鍵盤上讀入一行輸入,讀取輸入的函數(shù)是不可能每次都返回相同的結(jié)果的。而且,I/O本來(lái)就是要改變狀態(tài)的,它可以點(diǎn)亮終端上的像素,也可以讓打印機(jī)進(jìn)行輸出,甚至可以讓一個(gè)包裹從倉(cāng)庫(kù)發(fā)往另一塊陸地。I/O不光是改變程序的狀態(tài),它甚至改變了世界的狀態(tài)。 |
| 動(dòng)作 |
| 大多數(shù)語(yǔ)言并不對(duì)函數(shù)進(jìn)行純粹還是不純粹之分,但是Haskell的函數(shù)是數(shù)學(xué)意義上的函數(shù):它們就是純粹的計(jì)算,不能改變?nèi)魏瓮獠康氖挛铩4送?#xff0c;計(jì)算可以在任何時(shí)間進(jìn)行執(zhí)行,如果其結(jié)果永遠(yuǎn)沒(méi)有地方用到,那就不用進(jìn)行計(jì)算。 |
| 所以,我們需要些其他的工具來(lái)操作I/O。這些工具在Haskell中被稱做動(dòng)作。動(dòng)作和函數(shù)類似,定義的時(shí)候不做任何事情,只有被調(diào)用時(shí)才會(huì)執(zhí)行一些任務(wù)。I/O動(dòng)作通過(guò) IO monad 進(jìn)行定義。Monad是一種把函數(shù)串接起來(lái)的強(qiáng)大機(jī)制,會(huì)在第14章Monad 中進(jìn)行介紹。不過(guò)理解monad并不是理解I/O的必要條件,只要把I/O想象成打上了"IO"標(biāo)簽的動(dòng)作就可以了。我們來(lái)看幾個(gè)例子: |
| ghci> :type putStrLn |
| putStrLn :: String -> IO () |
| ghci> :type getLine |
| getLine :: IO String |
| putStrLn 的類型和一般的函數(shù)沒(méi)有什么兩樣,它接受一個(gè)參數(shù)并返回一個(gè) IO () 。 這個(gè) IO () 就是一個(gè)動(dòng)作。你還可以在純函數(shù)式代碼中存儲(chǔ)或傳遞動(dòng)作,不過(guò)這種情況并不常見(jiàn)。動(dòng)作在被調(diào)用之前什么都不做。再來(lái)看一個(gè)例子: |
| -- file: ch07/actions.hs |
| str2action :: String -> IO () |
| str2action input = putStrLn ("Data: " ++ input) |
| list2actions :: [String] -> [IO ()] |
| list2actions = map str2action |
| numbers :: [Int] |
| numbers = [1..10] |
| strings :: [String] |
| strings = map show numbers |
| actions :: [IO ()] |
| actions = list2actions strings |
| printitall :: IO () |
| printitall = runall actions |
| -- Take a list of actions, and execute each of them in turn. |
| runall :: [IO ()] -> IO () |
| runall [] = return () |
| runall (firstelem:remainingelems) = |
| do firstelem |
| runall remainingelems |
| main = do str2action "Start of the program" |
| printitall |
| str2action "Done!" |
| str2action 函數(shù)取一個(gè)參數(shù),并返回一個(gè) IO ()。在main 函數(shù)的末尾可以看出,可以直接在另一個(gè)動(dòng)作中對(duì)它進(jìn)行調(diào)用,它會(huì)立刻輸出一行。你也可以在純函數(shù)式代碼中存儲(chǔ)它但不能執(zhí)行。list2actions 函數(shù)就是這樣的例子,在 str2action 上使用 map ,返回一個(gè)動(dòng)作的列表,就和對(duì)待其他純的數(shù)據(jù)一樣。可以看到 printitall 函數(shù)完全是通過(guò)純函數(shù)實(shí)現(xiàn)的。 |
| 雖然定義了 printitall,但在它被求值并不會(huì)被執(zhí)行。注意在main中我們把 str2action當(dāng)作 I/O動(dòng)作來(lái)執(zhí)行,之前我們?cè)贗/O monad外面也使用過(guò)它來(lái)將結(jié)果組裝進(jìn)一個(gè)列表中。 |
| 你可以這樣來(lái)理解:do 程序塊中的每一個(gè)語(yǔ)句(除了 let)都必須產(chǎn)生一個(gè)I/O 動(dòng)作來(lái)執(zhí)行。 |
| 調(diào)用printitall將最終執(zhí)行所有的動(dòng)作。實(shí)際上,因?yàn)镠askell是惰性的,那些動(dòng)作也是到這個(gè)時(shí)候才被生成出來(lái)。 |
| 當(dāng)運(yùn)行這個(gè)程序時(shí),輸出如下: |
| Data: Start of the program |
| Data: 1 |
| Data: 2 |
| Data: 3 |
| Data: 4 |
| Data: 5 |
| Data: 6 |
| Data: 7 |
| Data: 8 |
| Data: 9 |
| Data: 10 |
| Data: Done! |
| 這個(gè)例程還可以用更簡(jiǎn)潔的方式來(lái)寫。看下面這個(gè): |
| -- file: ch07/actions2.hs |
| str2message :: String -> String |
| str2message input = "Data: " ++ input |
| str2action :: String -> IO () |
| str2action = putStrLn . str2message |
| numbers :: [Int] |
| numbers = [1..10] |
| main = do str2action "Start of the program" |
| mapM_ (str2action . show) numbers |
| str2action "Done!" |
| 注意 str2action 中函數(shù)組合操作符的使用。在 main 中有一個(gè)對(duì) mapM_ 的調(diào)用。這個(gè)函數(shù)類似 map。它取一個(gè)函數(shù)和一個(gè)列表作為輸入。提供給mapM_的函數(shù)是一個(gè)I/O動(dòng)作,它在列表中的每一個(gè)項(xiàng)目上進(jìn)行執(zhí)行。 mapM_ 拋棄動(dòng)作執(zhí)行的結(jié)果,當(dāng)然你也可以使用 mapM 來(lái)獲取IO動(dòng)作返回的結(jié)果。我們看一下它們的類型: |
| ghci> :type mapM |
| mapM :: (Monad m) => (a -> m b) -> [a] -> m [b] |
| ghci> :type mapM_ |
| mapM_ :: (Monad m) => (a -> m b) -> [a] -> m () |
| [Tip] Tip |
| 這些函數(shù)不止可以應(yīng)用在I/O上;它們也可以應(yīng)用在任何Monad上。現(xiàn)在,每當(dāng)看到"M",都要想到"IO"。而且,以下劃線結(jié)尾的函數(shù)一般都會(huì)將動(dòng)作的結(jié)果丟棄。 |
| 為什么有了map了還要有mapM呢?因?yàn)閙ap是純函數(shù),它返回一個(gè)列表,它不能直接執(zhí)行動(dòng)作。而mapM是IO monad中的工具,它可以直接執(zhí)行動(dòng)作。 |
| 回到main函數(shù),mapM_ 在numbers的每一個(gè)元素上調(diào)用 (str2action . show) 。show把一個(gè)數(shù)字轉(zhuǎn)換成字符串,str2action把一個(gè)字符串轉(zhuǎn)換成一個(gè)動(dòng)作。mapM_把這些單獨(dú)的動(dòng)作打包成一個(gè)系列的動(dòng)作,然后執(zhí)行他們,把數(shù)據(jù)打印出來(lái)。 |
| 順序 |
| do語(yǔ)句塊是把動(dòng)作連接起來(lái)的縮寫方式。有兩個(gè)操作符可以用來(lái)代替 do 語(yǔ)句塊: >> 和 >>=。在ghci中看下它們的類型: |
| ghci> :type (>>) |
| (>>) :: (Monad m) => m a -> m b -> m b |
| ghci> :type (>>=) |
| (>>=) :: (Monad m) => m a -> (a -> m b) -> m b |
| >> 操作符把兩個(gè)動(dòng)作順序連接在一起:先執(zhí)行第一個(gè)動(dòng)作,然后執(zhí)行第二個(gè)。最終返回第二個(gè)動(dòng)作執(zhí)行的結(jié)果。第一個(gè)動(dòng)作執(zhí)行的結(jié)果就被拋棄掉了。這類似于在do語(yǔ)句塊中包含簡(jiǎn)單的一行。可以用 putStrLn "line 1" >> putStrLn "line 2" 來(lái)試驗(yàn)下。它會(huì)打印出兩行,拋棄掉第一個(gè) putStrLn的結(jié)果,并給出第二個(gè)的結(jié)果。 |
| >>= 操作符執(zhí)行一個(gè)動(dòng)作,然后把它的結(jié)果傳遞給一個(gè)函數(shù),這個(gè)函數(shù)會(huì)返回一個(gè)動(dòng)作,然后這個(gè)動(dòng)作也被執(zhí)行,整個(gè)表達(dá)式的結(jié)果是第二個(gè)動(dòng)作的結(jié)果。可以用 getLine >>= putStrLn 作為一個(gè)例子,它從鍵盤讀入一行,然后顯示出來(lái)。 |
| 我們來(lái)重寫一個(gè)例子,讓它不包含do語(yǔ)句塊。還記得本章開始的這個(gè)例子么? |
| -- file: ch07/basicio.hs |
| main = do |
| putStrLn "Greetings! What is your name?" |
| inpStr <- getLine |
| putStrLn $ "Welcome to Haskell, " ++ inpStr ++ "!" |
| 我們不使用do語(yǔ)句塊來(lái)寫: |
| -- file: ch07/basicio-nodo.hs |
| main = |
| putStrLn "Greetings! What is your name?" >> |
| getLine >>= |
| (\inpStr -> putStrLn $ "Welcome to Haskell, " ++ inpStr ++ "!") |
| Haskell編譯器內(nèi)部會(huì)將do語(yǔ)句編譯成上面這種形式。 |
| [Tip] Tip |
| 忘記如何用 \ (lambda表達(dá)式)了?參考“匿名函數(shù)(lambda)”一節(jié)。 |
| return的本質(zhì) |
| 本章前面的時(shí)候有提到return在它的表象下面還有更深層的含義。很多語(yǔ)言中都有一個(gè)叫return的關(guān)鍵字,它們將會(huì)中斷當(dāng)前函數(shù)的執(zhí)行,并把結(jié)果返回給調(diào)用者。 |
| Haskell的return函數(shù)跟他們很不一樣。在Haskell里,return是用來(lái)把數(shù)據(jù)包裝到monad中的。具體到I/O的話,return就是用來(lái)把純的數(shù)據(jù)包裝到IO monad 中的。 |
| 那我們?yōu)槭裁匆M(jìn)行這樣的操作呢?記住任何依賴于 I/O的函數(shù)必須存在于 IO monad中。因此如果編寫一個(gè)函數(shù),它要執(zhí)行一些 I/O動(dòng)作,然后又要進(jìn)行一些純的計(jì)算操作,那就需要通過(guò) return 把純計(jì)算操作的返回結(jié)果轉(zhuǎn)換成適當(dāng)?shù)姆祷仡愋土恕7駝t要產(chǎn)生類型錯(cuò)誤的。比如: |
| -- file: ch07/return1.hs |
| import Data.Char(toUpper) |
| isGreen :: IO Bool |
| isGreen = |
| do putStrLn "Is green your favorite color?" |
| inpStr <- getLine |
| return ((toUpper . head $ inpStr) == 'Y') |
| 我們有一個(gè)純的計(jì)算,它產(chǎn)生一個(gè)布爾值。這個(gè)計(jì)算被傳遞給return,return把它放到 IO monad 中。因?yàn)樗?do語(yǔ)句塊的最后一個(gè)值,所以它成了 isGreen 的返回值,而不是因?yàn)槲覀兪褂昧藃eturn函數(shù)。 |
| 這是上面這個(gè)程序的另一個(gè)版本,它把純的計(jì)算提取到一個(gè)獨(dú)立的函數(shù)中。這有助于保持純函數(shù)式代碼的分離,并且使程序的意圖更加明確。 |
| -- file: ch07/return2.hs |
| import Data.Char(toUpper) |
| isYes :: String -> Bool |
| isYes inpStr = (toUpper . head $ inpStr) == 'Y' |
| isGreen :: IO Bool |
| isGreen = |
| do putStrLn "Is green your favorite color?" |
| inpStr <- getLine |
| return (isYes inpStr) |
| 最后,下面這個(gè)的例子展示了return并不一定要出現(xiàn)在do語(yǔ)句塊的末尾。在實(shí)踐中,它常常在末尾,但并非必需。 |
| -- file: ch07/return3.hs |
| returnTest :: IO () |
| returnTest = |
| do one <- return 1 |
| let two = 2 |
| putStrLn $ show (one + two) |
| 注意我們把 <- 和 return進(jìn)行組合,而 let 和字面量進(jìn)行組合。這是因?yàn)橐M(jìn)行相加操作,需要使用純的值。所以需要用 <- 把純的值從 monad 中“拉”出來(lái),它是return的逆運(yùn)算。在ghci中執(zhí)行它,可以看到顯示出了3。 |
| Haskell真的是命令式的么? |
| 這些do語(yǔ)句塊看上去很像命令式語(yǔ)言。畢竟,大多數(shù)時(shí)候你都是給出一些順序執(zhí)行的命令。 |
| 但是Haskell的核心依然是一種惰性語(yǔ)言。雖然經(jīng)常需要順序的執(zhí)行I/O動(dòng)作,但它是通過(guò)Haskell中已有的工具來(lái)進(jìn)行的。而且Haskell通過(guò) IO monad 將I/O和語(yǔ)言的其他部分很好地分離開來(lái)。 |
| 惰性I/O 的副作用 |
| 本章前面講到了 hGetContents。我們解釋說(shuō)它返回的 String 可以在純函數(shù)式代碼中使用。 |
| 我們需要對(duì)究竟什么是副作用做出更明確的解釋。我們說(shuō)的Haskell沒(méi)有副作用,它究竟是什么意思呢? |
| 在從某種意義上來(lái)說(shuō),“副作用”不可能完全避免的。一個(gè)有問(wèn)題的循環(huán),即使完全使用純函數(shù)式代碼進(jìn)行書寫,也有可能導(dǎo)致系統(tǒng)內(nèi)存耗盡并令機(jī)器崩潰,或者導(dǎo)致內(nèi)存數(shù)據(jù)被交換到磁盤。 |
| 當(dāng)我們說(shuō)沒(méi)有副作用時(shí),我們指的是Haskell中純的代碼不能執(zhí)行引發(fā)副作用的命令。純函數(shù)不能修改全局變量,不能進(jìn)行I/O請(qǐng)求,也不能執(zhí)行命令來(lái)破壞系統(tǒng)。 |
| 當(dāng)我們把hGetContents返回的String傳遞給純函數(shù),函數(shù)并不知道這個(gè)字符串是來(lái)自磁盤的文件。它還是表現(xiàn)得跟平常一樣,當(dāng)然處理那個(gè)String會(huì)導(dǎo)致I/O命令的執(zhí)行。但它們不是由純函數(shù)執(zhí)行的;它們是純函數(shù)進(jìn)行處理的過(guò)程所導(dǎo)致的一個(gè)結(jié)果,就和導(dǎo)致內(nèi)存和磁盤進(jìn)行交換的那個(gè)情況一樣。 |
| 在某些情況下,您可能需要對(duì)I/O何時(shí)發(fā)生進(jìn)行更精確的控制。也許你正在交互式地讀取用戶輸入的數(shù)據(jù),或者是通過(guò)管道從其他程序讀取輸入,這時(shí)需要直接與用戶通信。在這些情況下,hGetContents可能就不那么合適了。 |
| 緩沖 |
| I/O子系統(tǒng)是現(xiàn)代計(jì)算機(jī)中最慢的部分。寫磁盤花費(fèi)的時(shí)間比寫內(nèi)存高出上千倍。通過(guò)網(wǎng)絡(luò)寫入還要再慢上成百上千倍。即使你的操作不直接導(dǎo)致磁盤操作——比如說(shuō)因?yàn)閿?shù)據(jù)被緩存了——但I(xiàn)/O依然會(huì)進(jìn)行一下系統(tǒng)調(diào)用,還是會(huì)把速度拖慢了。 |
| 為此,現(xiàn)代的操作系統(tǒng)和編程語(yǔ)言都提供一些工具,幫助程序更高效的處理I/O。操作系統(tǒng)通常會(huì)進(jìn)行緩存,把常用的數(shù)據(jù)塊存放在內(nèi)存以便進(jìn)行快速存取。 |
| 編程語(yǔ)言通常也會(huì)進(jìn)行緩沖。這意味著即使代碼一次只處理一個(gè)字節(jié),它們也可能會(huì)向操作系統(tǒng)請(qǐng)求一大塊數(shù)據(jù)。這樣可以獲得很大的性能提升,因?yàn)槊看螌?duì)操作系統(tǒng)進(jìn)行I/O請(qǐng)求都會(huì)帶來(lái)很多開銷。緩沖可以讓我們讀取相同數(shù)量數(shù)據(jù)時(shí)產(chǎn)生的I/O請(qǐng)求少的多。 |
| Haskell也在它的I/O系統(tǒng)里提供了緩沖。很多情況下,緩沖是默認(rèn)的。只是在前面的章節(jié)中,我們一直沒(méi)有提到它的存在。Haskell一般情況都能夠很好地選取合適的默認(rèn)模式。但是這個(gè)默認(rèn)選擇很少是速度最快的。如果對(duì)你的代碼來(lái)說(shuō)I/O的速度很重要的話,手動(dòng)修改緩沖模式可以給程序的性能帶來(lái)很大提高。 |
| 緩沖模式 |
| Haskell中有三種緩沖模式:NoBuffering , LineBuffering ,和BlockBuffering,他們通過(guò) BufferMode 類型進(jìn)行定義。 |
| NoBuffering,正如它的名字所說(shuō):不進(jìn)行緩沖。使用 hGetLine 這樣的函數(shù)讀取數(shù)據(jù)的話,一次只會(huì)從操作系統(tǒng)讀取一個(gè)字符。寫數(shù)據(jù)的時(shí)候也會(huì)立刻數(shù)據(jù)把數(shù)據(jù)寫出去,并且經(jīng)常是一次只寫一個(gè)字符。因此NoBuffering通常性能很差,不適合一般用途使用。 |
| LineBuffering 在出現(xiàn)換行符或者數(shù)據(jù)太多時(shí),才將緩沖數(shù)據(jù)寫出,在輸入時(shí),它嘗試讀取數(shù)據(jù)塊的所有數(shù)據(jù)直到遇到換行符。當(dāng)從終端讀取時(shí),每當(dāng)按下回車時(shí)它就會(huì)立刻返回?cái)?shù)據(jù)。它往往是合理的默認(rèn)設(shè)置。 |
| BlockBuffering讓Haskell讀寫固定大小的數(shù)據(jù)塊。在處理大批量數(shù)據(jù)時(shí),它的性能是最好的,即使數(shù)據(jù)是面向行的。但是它不能用在交互程序上,因?yàn)樗趬K讀取滿之前是阻塞的。BlockBuffering接收一個(gè)Maybe類型參數(shù),如果是 Nothing,它使用Haskell實(shí)現(xiàn)預(yù)定義的緩沖區(qū)大小。或者,你可以用 Just 4096 這樣的設(shè)置來(lái)把緩沖區(qū)設(shè)置成4096個(gè)字節(jié)。 |
| 默認(rèn)的緩沖區(qū)模式取決于操作系統(tǒng)和Haskell的實(shí)現(xiàn)。可以調(diào)用 hGetBuffering 來(lái)詢問(wèn)系統(tǒng)當(dāng)前的緩沖模式。可以用hSetBuffering 設(shè)置當(dāng)前的緩沖模式,它接收一個(gè)句柄和一個(gè) BufferMode。例如,可以寫 hSetBuffering stdin (BlockBuffering Nothing)。 |
| 沖刷緩沖區(qū) |
| 不管是哪種類型的緩沖,都會(huì)常常需要強(qiáng)制把緩沖區(qū)的內(nèi)容寫出去。有時(shí)這會(huì)自動(dòng)進(jìn)行:比如調(diào)用 hClose的時(shí)候。但還是會(huì)有時(shí)候需要你手動(dòng)來(lái)調(diào)用 hFlush,這會(huì)強(qiáng)制把在緩沖區(qū)中等待的數(shù)據(jù)立刻寫出。如果句柄是網(wǎng)絡(luò)socket的話,這個(gè)操作會(huì)很有用,因?yàn)槟愠3?huì)需要立刻把數(shù)據(jù)寫出去。或者希望把數(shù)據(jù)寫到磁盤上,使得其他并發(fā)讀取它的程序可以立刻使用。 |
| 讀取命令行參數(shù) |
| 很多命令行程序需要處理傳給它的參數(shù)。System.Environment.getArgs 返回 IO [String] ,列出了每個(gè)參數(shù)。它類似于 C 語(yǔ)言argv在索引1之后的部分。程序名(C里的 argv[0])可以用 System.Environment.getProgName 獲得。 |
| System.Console.GetOpt 模塊提供了一些解析命令行選項(xiàng)的工具。如果你的程序擁有復(fù)雜的選項(xiàng),它就很有用了。在“命令行解析”一節(jié)中有使用它的例子。 |
| 環(huán)境變量 |
| 如果需要讀取環(huán)境變量,可以用System.Environment里面的兩個(gè)函數(shù):getEnv和getEnvironment。getEnv查看特定的變量,如果不存在就拋出異常。getEnvironment 把所有環(huán)境變量返回成 [(String, String)] 的值,之后就可以用lookup這類的函數(shù)來(lái)查找到你需要的環(huán)境變量了。 |
| 在Haskell中能夠跨平臺(tái)地設(shè)置環(huán)境變量的方法。如果在POSIX平臺(tái)上如Linux,你可以用System.Posix.Env模塊中的 putEnv 或者 setEnv。Windows上設(shè)置環(huán)境變量的方法沒(méi)有進(jìn)行定義。 |
| [15] 后面你會(huì)看到它還具有更廣泛的應(yīng)用,但現(xiàn)在考慮這幾條就夠了。 |
| [16] 值() 的類型也是()。 |
| [17] 命令式語(yǔ)言的程序員可能會(huì)擔(dān)心這樣的遞歸調(diào)用會(huì)消耗大量的桟空間。在Haskell里,遞歸是常見(jiàn)的用法,編譯器足夠聰明可以通過(guò)尾遞歸優(yōu)化來(lái)避免消耗太多桟空間。 |
| [18] 例如在混合程序的C語(yǔ)言部分存在bug。 |
| [19] 與其他程序通過(guò)管道進(jìn)行互操作的更詳細(xì)信息,請(qǐng)看“擴(kuò)展程序:管道”一節(jié)。 |
| [20] POSIX 程序員會(huì)有興趣知道它與C中的unlink()相對(duì)應(yīng)。 |
| [21] hGetContents 將在“惰性I/O”一節(jié)討論 |
| [22] 也有一個(gè)操作標(biāo)準(zhǔn)輸入的快捷函數(shù) getContents |
| [23] 更精確的說(shuō),它是從文件當(dāng)前位置到文件末尾的全部?jī)?nèi)容 |
| [24] I/O錯(cuò)誤如磁盤空間滿了 |
| [25] 技術(shù)上講,mapM把一組單獨(dú)的I/O動(dòng)作組合成一個(gè)大的動(dòng)作。大的動(dòng)作執(zhí)行時(shí)單獨(dú)的動(dòng)作被分別執(zhí)行。 |
轉(zhuǎn)載于:https://www.cnblogs.com/IBBC/archive/2011/07/25/2116321.html
總結(jié)
以上是生活随笔為你收集整理的Real World Haskell 第七章 I/O的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 迪拜游多少钱啊?
- 下一篇: 求一个qq网名社会男