日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當(dāng)前位置: 首頁 > 人文社科 > 生活经验 >内容正文

生活经验

Real World Haskell 第七章 I/O

發(fā)布時(shí)間:2023/11/27 生活经验 33 豆豆
生活随笔 收集整理的這篇文章主要介紹了 Real World Haskell 第七章 I/O 小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,幫大家做個(gè)參考.
幾乎所有程序都是用來從外部世界收集數(shù)據(jù),處理數(shù)據(jù),并把處理結(jié)果返回給外部世界的。也就是說,輸入和輸出對(duì)于程序設(shè)計(jì)來說相當(dāng)關(guān)鍵。
Haskell的I/O系統(tǒng)很強(qiáng)大,表達(dá)能力很強(qiáng)也很容易使用,理解它的原理對(duì)于學(xué)習(xí)Haskell來說非常重要。Haskell把純函數(shù)式代碼和那些會(huì)對(duì)外部世界產(chǎn)生影響的代碼嚴(yán)格區(qū)分了開來。也就是說它把副作用完全隔離在了純函數(shù)式的代碼之外。這樣不僅可以幫助程序員更容易驗(yàn)證程序的正確性,也能讓編譯器自動(dòng)進(jìn)行優(yōu)化和并行化。
本章先從Haskell簡(jiǎn)單的標(biāo)準(zhǔn)I/O開始。然后我們?cè)賮碛懻撘恍┢渌鼜?qiáng)大的做法,以及更詳細(xì)地探討I/O是如何與純粹、惰性、函數(shù)式的Haskell世界相融合的。
Haskell中的經(jīng)典I/O
我們先來看一個(gè)程序,它和其他語言如 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)輸入中讀入一行。你可能還不太了解 <- 的語法。簡(jiǎn)單地說,它就是把執(zhí)行I/O動(dòng)作的結(jié)果綁定到變量名上。然后我們用列表連接操作符 ++ 來把輸入的字符串和程序自己的文本連接起來。
我們來看一下 putStrLn 和 getLine 的類型,你可以從庫參考文檔中找到這個(gè)信息,或者直接問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)作可以通過更大的 I/O動(dòng)作結(jié)合在一起。()是一個(gè)空的元組(發(fā)音為 "unit"),表示從 putStrLn中沒有返回值。與 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)無縫的結(jié)合
* 執(zhí)行時(shí)產(chǎn)生副作用,但求值時(shí)不會(huì)。也就是說,只有在某個(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 的類型看上去可能有些奇怪。看起來它更像一個(gè)值而不是函數(shù)。實(shí)際上,可以這么看它:getLine存儲(chǔ)了一個(gè)I/O動(dòng)作。當(dāng)該動(dòng)作被執(zhí)行時(shí),返回一個(gè)String。<-操作符用來把結(jié)果從被執(zhí)行的動(dòng)作中“拉出來”,并存儲(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語法的完整解釋。
我們來看一個(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)說過的所有規(guī)則:當(dāng)給出相同輸入時(shí)總是返回相同的結(jié)果,沒有副作用,惰性求值。它用到了一些其他的Haskell函數(shù): (++), show, 和 length。
到后面的main中,我們把 name2reply inpStr 的值綁定到 outStr 變量。在do程序塊中時(shí),用 <- 獲得IO動(dòng)作的返回值,用 let 獲得純函數(shù)式代碼的返回值。在do程序塊中,你不能在let語句后面使用 in 。
在這段代碼中你可以看到如何從鍵盤上讀取一個(gè)人的名字。然后這個(gè)名字被傳遞給一個(gè)純函數(shù),然后其結(jié)果被輸出。實(shí)際上,main的最后兩行也可以用 putStrLn (name2reply inpStr) 來代替的。這樣盡管main 確實(shí)具有副作用(它讓終端上出現(xiàn)了一些字符),而name2reply 沒有并且不可能有副作用。因?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來顯示函數(shù)返回值的。但是使用putStrLn 的話,會(huì)把字符串發(fā)送給終端,終端則會(huì)把 \n 翻譯成換行符。
你覺得要是直接在ghci中輸入main 會(huì)發(fā)生什么呢?你可以自己試一下。
看過了這些例子程序之后,你可能會(huì)想Haskell其實(shí)是命令式的而不是純粹的、惰性的,函數(shù)式的。這些例子有些看上去好像就是一系列動(dòng)作按順序執(zhí)行。但是還里面確實(shí)還有更深刻的含義。我們將在本章后面部分的"Haskell真的是命令式的么?"和"惰性I/O"兩節(jié)中繼續(xù)探討這個(gè)問題。
Pure vs. I/O
為了幫助理解純函數(shù)式代碼和I/O之間究竟有何不同,這里給出一個(gè)對(duì)照表。當(dāng)我們說純函數(shù)式代碼時(shí),我們說的是那些對(duì)相同輸入總是返回相同結(jié)果,并且沒有副作用的Haskell函數(shù)。在Haskell里,只有I/O動(dòng)作的執(zhí)行不適用于這些規(guī)則。
Table 7.1. Pure vs. Impure
Pure Impure
純 非純
給定相同參數(shù)總是返回相同值 對(duì)相同參數(shù)可能返回不通值
永遠(yuǎn)沒有副作用 可以有副作用
用于不改變狀態(tài) 可以改變程序,系統(tǒng)或外界的全局狀態(tài)
為什么純粹性如此重要
這一節(jié)中我們已經(jīng)探討了Haskell是如何將純函數(shù)式代碼與 I/O動(dòng)作清楚的分離開來的。大多數(shù)語言并不會(huì)這樣區(qū)分。像 C 或 Java這樣的語言里,編譯器不能保證某一個(gè)函數(shù)對(duì)相同的參數(shù)總是返回相同的值,或者保證一個(gè)函數(shù)永遠(yuǎn)沒有副作用。要想知道一個(gè)函數(shù)是否有副作用,唯一的辦法就是去讀它的文檔,而這文檔還不一定準(zhǔn)確。
程序中很多bug都是由一些出乎意料的副作用導(dǎo)致的。還有一些就是因?yàn)楸灰粋€(gè)函數(shù)對(duì)相同的輸入返回不同的結(jié)果給搞糊涂了。隨著多線程和其他形式的并行變得越來越平常,要管理全局的副作用就變得愈加困難了。
Haskell這種把副作用隔離進(jìn)I/O動(dòng)作的方法提供了一個(gè)清楚的邊界。你總是可以清楚地知道系統(tǒng)的哪一部分可能會(huì)修改狀態(tài),哪些不會(huì)。你總是可以確信程序中純函數(shù)式的那部分代碼不會(huì)產(chǎn)生出人意料的結(jié)果。這可以幫助你編寫程序。同樣也可以幫助編譯器來理解你的程序。例如最近一些版本的ghc就可以對(duì)代碼中純函數(shù)式的部分——這部分代碼可說是計(jì)算中的圣杯——提供一定程度的自動(dòng)的并行處理。
關(guān)于這個(gè)主題的更多討論,見 “惰性I/O的副作用”一節(jié)。
操作文件和句柄
現(xiàn)在你已經(jīng)看過如何通過計(jì)算機(jī)終端與用戶進(jìn)行交互了。當(dāng)然,你經(jīng)常會(huì)需要操作一些特定的文件。這個(gè),同樣很容易做到。
Haskell為I/O定義了很多基本函數(shù),他們中的許多都與其他編程語言類似。System.IO 的庫參考文檔提供了所有基本I/O函數(shù)的概述,如果你需要某個(gè)在本文中沒有涉及到的函數(shù),可以到參考哪里。
操作文件,一般從 openFile 開始,它會(huì)返回給你一個(gè)文件句柄。然后你就可以用這個(gè)句柄對(duì)那個(gè)文件進(jìn)行操作。Haskell提供了諸如hPutStrLn 這樣的函數(shù),它類似putStrLn,不過需要多傳一個(gè)文件句柄參數(shù),指定要操作的文件。操作完用 hClose來關(guān)閉句柄。這些函數(shù)都定義在 System.IO 中,因此在操作文件之前要先導(dǎo)入這個(gè)模塊。差不多所有非"h"開頭的函數(shù)都有與之相對(duì)的 "h"開頭的函數(shù);例如有一個(gè) print 用來向屏幕輸出,就有一個(gè)hPrint用來向文件輸出。
我們先來用命令式的方式來對(duì)文件進(jìn)行讀寫,應(yīng)該和其他語言里面的while循環(huán)有些類似。不過這并不是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不一樣。在那些語言里,return用來立即中止當(dāng)前函數(shù)的執(zhí)行,并把值返回給調(diào)用者。在Haskell里,return與 <- 恰好相反。也就是說return把一個(gè)純的值包裝成一個(gè)IO類型。因?yàn)槊恳粋€(gè)I/O動(dòng)作必須返回IO類型,如果你的結(jié)果來自純的計(jì)算,必須把它包裝成IO類型再返回。比如說,對(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來檢查下 openFile的類型:
ghci> :module System.IO
ghci> :type openFile
openFile :: FilePath -> IOMode -> IO Handle
FilePath 只是String的一個(gè)別名。在I/O函數(shù)中使用它而非String是為了指明該參數(shù)是特別用來作文件名用的,而不是一個(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是用來關(guān)閉文件句柄的。讓我們花點(diǎn)時(shí)間來探討下為什么關(guān)閉句柄很重要。
在“緩沖”一節(jié)你將會(huì)看到,Haskell為文件維護(hù)了內(nèi)部的緩沖區(qū)。這帶來了很關(guān)鍵的性能提升。但是,這樣一來的話,以寫入模式打開的文件,可能要到調(diào)用hClose的時(shí)候,有些數(shù)據(jù)才會(huì)真正被寫到操作系統(tǒng)上去。
要確保對(duì)打開的文件調(diào)用hClose的另一個(gè)原因是它會(huì)占用系統(tǒng)資源。如果你的程序執(zhí)行很長(zhǎng)時(shí)間,并且打開了很多文件但是沒有關(guān)閉他們,你的程序很可能因?yàn)橘Y源耗盡而崩潰。這一點(diǎn)上Haskell與其他語言沒什么區(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)通過句柄從磁盤讀寫文件的時(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)有三種,用來表示如何對(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)行偏移的。一般來說句柄都是指向文件,但是它也能夠指向其他一些不能進(jìn)行偏移操作的東西,例如網(wǎng)絡(luò)連接,磁帶驅(qū)動(dòng)器,或者終端。可以用hIsSeekable 來檢查一個(gè)給定的句柄是否支持偏移。
標(biāo)準(zhǔn)輸入,標(biāo)準(zhǔn)輸出,標(biāo)準(zhǔn)錯(cuò)誤
之前我們提到過每一個(gè)非"h"的函數(shù),一般都有一個(gè) "h"函數(shù)與之對(duì)應(yīng),可以用來處理任何句柄。實(shí)際上,非"h"的函數(shù)只不過是他們的"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ò)誤一般也指向顯示屏。
我們可以這樣來定義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上也可以使用。
一般來說使用標(biāo)準(zhǔn)輸入輸出而非顯示指定文件是有好處的,這樣你可以通過終端和用戶進(jìn)行交互。同時(shí)也允許你操作輸入輸出文件,如果需要的話甚至還可以和其他程序組合在一起。
舉個(gè)例子來說,你可以這種方式給 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í)不同,輸出中沒有John的那一行。終端把你鍵入的內(nèi)容回顯給你,但這是通過另一個(gè)程序進(jìn)行輸入,不會(huì)把輸入包含在輸出流里。
刪除和重命名文件
本章前面部分探討了如何操作文件的內(nèi)容。現(xiàn)在讓我們來關(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ù)用來進(jìn)行目錄的創(chuàng)建和刪除,獲取目錄中的文件列表,檢測(cè)文件是否存在。在“目錄和文件信息”一節(jié)將會(huì)對(duì)這些話題進(jìn)行探討。
臨時(shí)文件
程序員經(jīng)常需要?jiǎng)?chuàng)建臨時(shí)文件。這些文件可以用來存儲(chǔ)計(jì)算需要的大量數(shù)據(jù),或者是供給其他程序或其他用戶使用的數(shù)據(jù)。
你可以手動(dòng)為創(chuàng)建的文件取一個(gè)獨(dú)一無二的文件名,但是在不同的平臺(tái)上安全地做到這一點(diǎn)還是需要處理一些細(xì)節(jié)上的不同。Haskell提供了一個(gè)方便的函數(shù)叫 openTempFile (和對(duì)應(yīng)的openBinaryTempFile),可以幫你處理這個(gè)問題。
openTempFile 需要兩個(gè)參數(shù):要?jiǎng)?chuàng)建文件的目錄和文件名命名的“模板”。目錄可以直接用 "."表示當(dāng)前工作目錄。或者使用System.Directory.getTemporaryDirectory得到機(jī)器上的臨時(shí)目錄。文件名模板作為創(chuàng)建文件名的基礎(chǔ),然后再添加一些隨機(jī)字符上去,以確保產(chǎn)生的文件名是真正獨(dú)一無二的。
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é),甚至一些還沒見過的概念。嘗試閱讀本程序,看看能否看出它是做什么的,以及是如何去做的。
-- 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í)文件用通過 openTempFile 創(chuàng)建的。目錄是 getTemporaryDirectory 所指定的,如果系統(tǒng)沒有臨時(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).
庫參考手冊(cè)里說如果系統(tǒng)不支持臨時(shí)目錄概念的話,getTemporaryDirectory 將會(huì)拋出異常。
因此我們?cè)?catch 之下來執(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)
我們來看下程序的結(jié)尾。從withTempFile這個(gè)函數(shù)可以看出Haskell在引入I/O的時(shí)候并沒有忘記它自己函數(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)。異常處理更多信息見第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é)的顯示出來。之后又通過 print c 來按Haskell的字面量格式輸出出來,最后這個(gè)操作等價(jià)于 putStrLn (show c) 相同。
讓我們來看看輸出:
$ 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ì)有如下一些疑問:
1、為什么寫入了22個(gè)字節(jié)后,位置是23?
2、為什么文件內(nèi)容最后顯示有一個(gè)空行?
3、為什么在Haskell字面格式顯示的末尾有一個(gè) \n
你可能已經(jīng)猜到這三個(gè)問題的互相之間都是有關(guān)聯(lián)的。你可以先自己想一會(huì),看能否找出答案。如果需要幫助,這里也為你提供一些解釋:
1、因?yàn)槲覀兪怯玫?hPutStrLn 而不是 hPutStr 來寫入數(shù)據(jù)。hPutStrLn 總是會(huì)在行末尾添加 \n,這個(gè) \n 在 tempdate 中并不存在。
2、這里是用 putStrLn c 來顯示文件內(nèi)容 c 。因?yàn)槲募?nèi)容本來就是通過 hPutStrLn 寫入的,所以 c 的末尾是個(gè)換行符,而 putStrLn 在輸出的時(shí)候又在末尾添加了一個(gè)換行符,結(jié)果就顯示出了一個(gè)空行。
3、\n 就是一開始hPutStrLn輸出的換行符。
最后說明下,在不同的操作系統(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是惰性語言,也就是說只有到最后關(guān)頭才會(huì)對(duì)數(shù)據(jù)的值進(jìn)行求值,所以這里會(huì)一些新穎的處理I/O的方法。
hGetContents
新方法之一就是hGetContents函數(shù)。hGetContents 的類型是 Handle -> IO String。返回的 String 的值就是指定文件句柄中的全部?jī)?nèi)容。
在嚴(yán)格求值的語言里,使用這樣的函數(shù)往往是不太好的。因?yàn)樽x取一個(gè)2KB的文件還可以,要是讀取一個(gè)500GB文件的全部?jī)?nèi)容,將很可能會(huì)因?yàn)閮?nèi)存不夠而崩潰。在這些語言中,需要用傳統(tǒng)的機(jī)制如循環(huán)來處理文件的全部?jī)?nèi)容。
但是hGetContents是不一樣的。它返回的String是惰性求值的。也就是說在調(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ù)式的代碼(沒有IO)。
我們來看一個(gè)例子。前一節(jié)“處理文件和句柄”中有一個(gè)命令式的程序,把文件的全部?jī)?nèi)容轉(zhuǎ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)樗鼪]有副作用,并且使用相同的參數(shù)調(diào)用總是返回相同的結(jié)果。它不需要——也沒辦法——知道它的輸入是從文件中惰性讀取的。不管是20個(gè)字符的字符串還是磁盤上存的500GB的數(shù)據(jù),它都可以完美地進(jìn)行處理。
可以使用ghci來確認(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)潔的版本,接下來將以這個(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)文件。從文件讀出來的數(shù)據(jù)也是一樣的處理。當(dāng)一段數(shù)據(jù)不再需要的時(shí)候,Haskell環(huán)境將會(huì)釋放占有的內(nèi)存。嚴(yán)格地說在這個(gè)例子里根本不需要調(diào)用hClose。不過加上它仍然是一個(gè)好習(xí)慣,因?yàn)閷韺?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用作過濾器。他們從一個(gè)文件讀取數(shù)據(jù),對(duì)數(shù)據(jù)進(jìn)行一些處理,把結(jié)果寫出到其他地方。因?yàn)檫@個(gè)模式如此常見,所以就產(chǎn)生了一些更快捷的方式。readFile 和 writeFile 就是這樣的快捷方式,他們可以把文件直接當(dāng)作字符串進(jìn)行處理。它們內(nèi)部會(huì)處理好打開文件,關(guān)閉文件,讀寫數(shù)據(jù)的所有細(xì)節(jié)問題。readFile內(nèi)部就是使用hGetContents的。
你能猜出這些函數(shù)的類型么?我們用ghci來看看:
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都沒有向你暴露文件句柄,因此也就不需要手動(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í)候把他們寫入到輸出文件,但它們沒有必要保留那些已經(jīng)被寫出的數(shù)據(jù),只要程序中沒有其他部分需要這些數(shù)據(jù),它們的內(nèi)存就會(huì)被立刻釋放。某種意義上來說,你可以把在readFile和writeFile 間傳遞的字符串想象成連接兩者的管道。數(shù)據(jù)從一端,經(jīng)過一些變形,流向另一端。
你可以給 toupper-lazy3.hs 生成一個(gè)大的 input.txt,來自己驗(yàn)證這一點(diǎn)。這會(huì)需要一點(diǎn)時(shí)間來處理,但是在它處理過程中你會(huì)看到一個(gè)很低且穩(wěn)定的內(nèi)存占用。
interact
我們已經(jīng)學(xué)到readFile 和 writeFile 是如何處理讀取文件,進(jìn)行轉(zhuǎn)換,再寫到另一個(gè)文件的這種常見情況。還有比這更常見的一種情況:從標(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ì)通過 getContents 惰性的讀入標(biāo)準(zhǔn)輸入,并將結(jié)果傳入這個(gè)函數(shù),再把這個(gè)函數(shù)的返回發(fā)送到標(biāo)準(zhǔn)輸出。
我們可以把我們上面的例程轉(zhuǎn)換成通過interact來操作標(biāo)準(zhǔn)輸入輸出,像下面這樣:
-- file: ch07/toupper-lazy4.hs
import Data.Char(toUpper)
main = interact (map toUpper)
看,我們只用一行代碼就完成了轉(zhuǎn)換!要得到和之前例子相同的效果,需要像下面這樣來執(zhí)行:
$ runghc toupper-lazy4.hs < input.txt > output.txt
或者,如果想在屏幕上看到輸出,可以這樣:
$ runghc toupper-lazy4.hs < input.txt
如果你希望程序交互性地處理輸入輸出的話,執(zhí)行 runghc toupper-lazy4.hs 即可,不要帶其他命令行參數(shù)。可以看到每輸入一個(gè)字符,它都用大寫形式回顯出來。不過這種情況下不同緩沖模式可能會(huì)對(duì)程序的具體行為有所影響,關(guān)于緩沖的更多信息參見本章后面的“緩沖”一節(jié)。如果你遇到輸入一行結(jié)束時(shí)才回顯,或者暫時(shí)沒有進(jìn)行回顯,那就是緩沖惹得禍了。
我們也可以用 interact 來編寫一些簡(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ì) . 操作符的使用感到迷惑,不妨參考下“通過函數(shù)組合重用代碼”一節(jié)。
這里我們?cè)谳敵龅拈_頭添加了一個(gè)字符串。但是你可以看出這里面有什么問題么?
因?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 做過濾器
interact也常常用作過濾器。假設(shè)你要讀入一個(gè)文件,并將所有包含字母"a"的每一行打印出來。這里是一個(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
你可以通過它們的類型猜出這些函數(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ù)來運(yùn)行它:
$ runghc filter.hs < input.txt
I like Haskell
Haskell is great
果然輸出了包含 "a" 字符的兩行。惰性過濾器是Haskell中強(qiáng)大的處理方式。可以想想看,一個(gè)過濾器——如標(biāo)準(zhǔn)Unix程序 grep——聽上去就像一個(gè)函數(shù)。它獲取一些輸入,進(jìn)行一些計(jì)算,產(chǎn)生一些可預(yù)測(cè)的輸出。
IO Monad
前面我們已經(jīng)看了很多使用Haskell語言進(jìn)行I/O處理的例子,下面讓我們退一步,來考慮下I/O是如何與更廣泛的Haskell語言進(jìn)行融合的。
因?yàn)镠askell是一種純函數(shù)的語言,每次用相同的參數(shù)調(diào)用一個(gè)函數(shù)都會(huì)返回相同的結(jié)果。而且,函數(shù)也不會(huì)改變程序的全局狀態(tài)。
那您可能會(huì)好奇I/O是如何融合進(jìn)這樣一種情況的呢,因?yàn)楹茱@然,如果從鍵盤上讀入一行輸入,讀取輸入的函數(shù)是不可能每次都返回相同的結(jié)果的。而且,I/O本來就是要改變狀態(tài)的,它可以點(diǎn)亮終端上的像素,也可以讓打印機(jī)進(jìn)行輸出,甚至可以讓一個(gè)包裹從倉庫發(fā)往另一塊陸地。I/O不光是改變程序的狀態(tài),它甚至改變了世界的狀態(tài)。
動(dòng)作
大多數(shù)語言并不對(duì)函數(shù)進(jìn)行純粹還是不純粹之分,但是Haskell的函數(shù)是數(shù)學(xué)意義上的函數(shù):它們就是純粹的計(jì)算,不能改變?nèi)魏瓮獠康氖挛铩4送?#xff0c;計(jì)算可以在任何時(shí)間進(jìn)行執(zhí)行,如果其結(jié)果永遠(yuǎn)沒有地方用到,那就不用進(jìn)行計(jì)算。
所以,我們需要些其他的工具來操作I/O。這些工具在Haskell中被稱做動(dòng)作。動(dòng)作和函數(shù)類似,定義的時(shí)候不做任何事情,只有被調(diào)用時(shí)才會(huì)執(zhí)行一些任務(wù)。I/O動(dòng)作通過 IO monad 進(jìn)行定義。Monad是一種把函數(shù)串接起來的強(qiáng)大機(jī)制,會(huì)在第14章Monad 中進(jìn)行介紹。不過理解monad并不是理解I/O的必要條件,只要把I/O想象成打上了"IO"標(biāo)簽的動(dòng)作就可以了。我們來看幾個(gè)例子:
ghci> :type putStrLn
putStrLn :: String -> IO ()
ghci> :type getLine
getLine :: IO String
putStrLn 的類型和一般的函數(shù)沒有什么兩樣,它接受一個(gè)參數(shù)并返回一個(gè) IO () 。 這個(gè) IO () 就是一個(gè)動(dòng)作。你還可以在純函數(shù)式代碼中存儲(chǔ)或傳遞動(dòng)作,不過這種情況并不常見。動(dòng)作在被調(diào)用之前什么都不做。再來看一個(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ù)完全是通過純函數(shù)實(shí)現(xiàn)的。
雖然定義了 printitall,但在它被求值并不會(huì)被執(zhí)行。注意在main中我們把 str2action當(dāng)作 I/O動(dòng)作來執(zhí)行,之前我們?cè)贗/O monad外面也使用過它來將結(jié)果組裝進(jìn)一個(gè)列表中。
你可以這樣來理解:do 程序塊中的每一個(gè)語句(除了 let)都必須產(chǎn)生一個(gè)I/O 動(dòng)作來執(zhí)行。
調(diào)用printitall將最終執(zhí)行所有的動(dòng)作。實(shí)際上,因?yàn)镠askell是惰性的,那些動(dòng)作也是到這個(gè)時(shí)候才被生成出來。
當(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)潔的方式來寫。看下面這個(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 來獲取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ù)打印出來。
順序
do語句塊是把動(dòng)作連接起來的縮寫方式。有兩個(gè)操作符可以用來代替 do 語句塊: >> 和 >>=。在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語句塊中包含簡(jiǎn)單的一行。可以用 putStrLn "line 1" >> putStrLn "line 2" 來試驗(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è)例子,它從鍵盤讀入一行,然后顯示出來。
我們來重寫一個(gè)例子,讓它不包含do語句塊。還記得本章開始的這個(gè)例子么?
-- file: ch07/basicio.hs
main = do
putStrLn "Greetings! What is your name?"
inpStr <- getLine
putStrLn $ "Welcome to Haskell, " ++ inpStr ++ "!"
我們不使用do語句塊來寫:
-- file: ch07/basicio-nodo.hs
main =
putStrLn "Greetings! What is your name?" >>
getLine >>=
(\inpStr -> putStrLn $ "Welcome to Haskell, " ++ inpStr ++ "!")
Haskell編譯器內(nèi)部會(huì)將do語句編譯成上面這種形式。
[Tip] Tip
忘記如何用 \ (lambda表達(dá)式)了?參考“匿名函數(shù)(lambda)”一節(jié)。
return的本質(zhì)
本章前面的時(shí)候有提到return在它的表象下面還有更深層的含義。很多語言中都有一個(gè)叫return的關(guān)鍵字,它們將會(huì)中斷當(dāng)前函數(shù)的執(zhí)行,并把結(jié)果返回給調(diào)用者。
Haskell的return函數(shù)跟他們很不一樣。在Haskell里,return是用來把數(shù)據(jù)包裝到monad中的。具體到I/O的話,return就是用來把純的數(shù)據(jù)包裝到IO monad 中的。
那我們?yōu)槭裁匆M(jìn)行這樣的操作呢?記住任何依賴于 I/O的函數(shù)必須存在于 IO monad中。因此如果編寫一個(gè)函數(shù),它要執(zhí)行一些 I/O動(dòng)作,然后又要進(jìn)行一些純的計(jì)算操作,那就需要通過 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語句塊的最后一個(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語句塊的末尾。在實(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 中“拉”出來,它是return的逆運(yùn)算。在ghci中執(zhí)行它,可以看到顯示出了3。
Haskell真的是命令式的么?
這些do語句塊看上去很像命令式語言。畢竟,大多數(shù)時(shí)候你都是給出一些順序執(zhí)行的命令。
但是Haskell的核心依然是一種惰性語言。雖然經(jīng)常需要順序的執(zhí)行I/O動(dòng)作,但它是通過Haskell中已有的工具來進(jìn)行的。而且Haskell通過 IO monad 將I/O和語言的其他部分很好地分離開來。
惰性I/O 的副作用
本章前面講到了 hGetContents。我們解釋說它返回的 String 可以在純函數(shù)式代碼中使用。
我們需要對(duì)究竟什么是副作用做出更明確的解釋。我們說的Haskell沒有副作用,它究竟是什么意思呢?
在從某種意義上來說,“副作用”不可能完全避免的。一個(gè)有問題的循環(huán),即使完全使用純函數(shù)式代碼進(jìn)行書寫,也有可能導(dǎo)致系統(tǒng)內(nèi)存耗盡并令機(jī)器崩潰,或者導(dǎo)致內(nèi)存數(shù)據(jù)被交換到磁盤。
當(dāng)我們說沒有副作用時(shí),我們指的是Haskell中純的代碼不能執(zhí)行引發(fā)副作用的命令。純函數(shù)不能修改全局變量,不能進(jìn)行I/O請(qǐng)求,也不能執(zhí)行命令來破壞系統(tǒng)。
當(dāng)我們把hGetContents返回的String傳遞給純函數(shù),函數(shù)并不知道這個(gè)字符串是來自磁盤的文件。它還是表現(xiàn)得跟平常一樣,當(dāng)然處理那個(gè)String會(huì)導(dǎo)致I/O命令的執(zhí)行。但它們不是由純函數(shù)執(zhí)行的;它們是純函數(shù)進(jìn)行處理的過程所導(dǎo)致的一個(gè)結(jié)果,就和導(dǎo)致內(nèi)存和磁盤進(jìn)行交換的那個(gè)情況一樣。
在某些情況下,您可能需要對(duì)I/O何時(shí)發(fā)生進(jìn)行更精確的控制。也許你正在交互式地讀取用戶輸入的數(shù)據(jù),或者是通過管道從其他程序讀取輸入,這時(shí)需要直接與用戶通信。在這些情況下,hGetContents可能就不那么合適了。
緩沖
I/O子系統(tǒng)是現(xiàn)代計(jì)算機(jī)中最慢的部分。寫磁盤花費(fèi)的時(shí)間比寫內(nèi)存高出上千倍。通過網(wǎng)絡(luò)寫入還要再慢上成百上千倍。即使你的操作不直接導(dǎo)致磁盤操作——比如說因?yàn)閿?shù)據(jù)被緩存了——但I(xiàn)/O依然會(huì)進(jìn)行一下系統(tǒng)調(diào)用,還是會(huì)把速度拖慢了。
為此,現(xiàn)代的操作系統(tǒng)和編程語言都提供一些工具,幫助程序更高效的處理I/O。操作系統(tǒng)通常會(huì)進(jìn)行緩存,把常用的數(shù)據(jù)塊存放在內(nèi)存以便進(jìn)行快速存取。
編程語言通常也會(huì)進(jìn)行緩沖。這意味著即使代碼一次只處理一個(gè)字節(jié),它們也可能會(huì)向操作系統(tǒng)請(qǐng)求一大塊數(shù)據(jù)。這樣可以獲得很大的性能提升,因?yàn)槊看螌?duì)操作系統(tǒng)進(jìn)行I/O請(qǐng)求都會(huì)帶來很多開銷。緩沖可以讓我們讀取相同數(shù)量數(shù)據(jù)時(shí)產(chǎn)生的I/O請(qǐng)求少的多。
Haskell也在它的I/O系統(tǒng)里提供了緩沖。很多情況下,緩沖是默認(rèn)的。只是在前面的章節(jié)中,我們一直沒有提到它的存在。Haskell一般情況都能夠很好地選取合適的默認(rèn)模式。但是這個(gè)默認(rèn)選擇很少是速度最快的。如果對(duì)你的代碼來說I/O的速度很重要的話,手動(dòng)修改緩沖模式可以給程序的性能帶來很大提高。
緩沖模式
Haskell中有三種緩沖模式:NoBuffering , LineBuffering ,和BlockBuffering,他們通過 BufferMode 類型進(jìn)行定義。
NoBuffering,正如它的名字所說:不進(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è)置來把緩沖區(qū)設(shè)置成4096個(gè)字節(jié)。
默認(rèn)的緩沖區(qū)模式取決于操作系統(tǒng)和Haskell的實(shí)現(xiàn)。可以調(diào)用 hGetBuffering 來詢問系統(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)來調(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 語言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ù)來查找到你需要的環(huán)境變量了。
在Haskell中能夠跨平臺(tái)地設(shè)置環(huán)境變量的方法。如果在POSIX平臺(tái)上如Linux,你可以用System.Posix.Env模塊中的 putEnv 或者 setEnv。Windows上設(shè)置環(huán)境變量的方法沒有進(jìn)行定義。
[15] 后面你會(huì)看到它還具有更廣泛的應(yīng)用,但現(xiàn)在考慮這幾條就夠了。
[16] 值() 的類型也是()。
[17] 命令式語言的程序員可能會(huì)擔(dān)心這樣的遞歸調(diào)用會(huì)消耗大量的桟空間。在Haskell里,遞歸是常見的用法,編譯器足夠聰明可以通過尾遞歸優(yōu)化來避免消耗太多桟空間。
[18] 例如在混合程序的C語言部分存在bug。
[19] 與其他程序通過管道進(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] 更精確的說,它是從文件當(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ǎng)站內(nèi)容還不錯(cuò),歡迎將生活随笔推薦給好友。