第九章:I/O學(xué)習(xí) —— 構(gòu)建一個(gè)用于搜索文件系統(tǒng)的庫(kù)

2018-02-24 15:49 更新

第九章:I/O學(xué)習(xí) —— 構(gòu)建一個(gè)用于搜索文件系統(tǒng)的庫(kù)

自從電腦有了分層文件系統(tǒng)以來(lái),“我知道有這個(gè)文件,但不知道它放在哪”這個(gè)問(wèn)題就一直困擾著人們。1974年發(fā)布的Unix第五個(gè)版本引入的 find 命令,到今天仍在使用。查找文件的藝術(shù)已經(jīng)走過(guò)了很長(zhǎng)一段路:伴隨現(xiàn)代操作系統(tǒng)一起不斷發(fā)展的文件索引和搜索功能。

給程序員的工具箱里添加類似 find 這樣的功能依舊非常有價(jià)值,在本章,我們將通過(guò)編寫(xiě)一個(gè)Haskell庫(kù)給我們的 find 命令添加更多功能,我們將通過(guò)一些有著不同的健壯度的方法來(lái)完成這個(gè)庫(kù)。

find命令

如果你不曾用過(guò)類Unix的系統(tǒng),或者你不是個(gè)重度shell用戶,那么你很可能從未聽(tīng)說(shuō)過(guò) find ,通過(guò)給定的一組目錄,它遞歸搜索每個(gè)目錄并且打印出每個(gè)匹配表達(dá)式的實(shí)體名稱。

-- file: ch09/RecursiveContents.hs
module RecursiveContents (getRecursiveContents) where
import Control.Monad (forM)
import System.Directory (doesDirectoryExist, getDirectoryContents)
import System.FilePath ((</>))
getRecursiveContents :: FilePath -> IO [FilePath]
getRecursiveContents topdir = do
    names <- getDirectoryContents topdir
let properNames = filter (`notElem` [".", ".."]) names
    paths <- forM properNames $ \name -> do
let path = topdir </> name
    isDirectory <- doesDirectoryExist path
    if isDirectory
        then getRecursiveContents path
    else return [path]
return (concat paths)

單個(gè)表達(dá)式可以識(shí)別像“符合這個(gè)全局模式的名稱”,“實(shí)體是一個(gè)文件”,“當(dāng)前最后一個(gè)被修改的文件”以及其他諸如此類的表達(dá)式,通過(guò)and或or算子就可以把他們裝配起來(lái)構(gòu)成更加復(fù)雜的表達(dá)式

簡(jiǎn)單的開(kāi)始:遞歸遍歷目錄

在投入設(shè)計(jì)我們的庫(kù)之前,先解決一些規(guī)模稍小的問(wèn)題,我們第一個(gè)問(wèn)題就是遞歸地列出一個(gè)目錄下面的所有內(nèi)容和它的子目錄

filter 表達(dá)式確保一個(gè)目錄的列表不含特定的目錄名(比如代表當(dāng)前目錄的 . 和上一級(jí)目錄的 .. ),如果忘記過(guò)濾這些,隨后的查找將陷入無(wú)限循環(huán)。

我們?cè)谥暗恼鹿?jié)里完成了 forM 函數(shù),它是參數(shù)顛倒后的 mapM 函數(shù)。

ghci> :m +Control.Monad
ghci> :type mapM
mapM :: (Monad m) => (a -> m b) -> [a] -> m [b]
ghci> :type forM
forM :: (Monad m) => [a] -> (a -> m b) -> m [b]

循環(huán)體將檢查當(dāng)前實(shí)體是否為目錄,如果是,則遞歸調(diào)用 getrecuresivacontents 函數(shù)列出這個(gè)目錄(的內(nèi)容),如果否,則返回只含有當(dāng)前實(shí)體名稱一個(gè)元素的列表,不要忘記 return 函數(shù)在 Haskell 中有特殊的含義,他通過(guò)monad的類型構(gòu)造器包裝了一個(gè)值。

另一個(gè)值得注意的地方是變量 isDirectory 的使用,在命令式語(yǔ)言如 Python 中,我們通常用 ifos.path.isdir(path) 來(lái)表示,然而, doesDirectoryExist 函數(shù)是一個(gè)動(dòng)作,它的返回類型是 IOBool 而非 Bool ,由于 if 表達(dá)式需要一個(gè)操作值為 bool 的表達(dá)式作為條件,我們使用 <- 來(lái)從io包裝器上得到這個(gè)動(dòng)作的的 bool 返回值,這樣我們就能在 if 中使用這個(gè)干凈的無(wú)包裝的 bool 。

循環(huán)體中每一次迭代生成的結(jié)果都是名稱列表,因此 forM 的結(jié)果是 IO[[FilePath]] ,我們通過(guò) concat 將它轉(zhuǎn)換為一個(gè)元素列表(從以列表為元素的列表轉(zhuǎn)換為不含列表元素的列表)

再次認(rèn)識(shí)匿名和命名函數(shù)

Anonymous (lambda) functions [http://book.realworldhaskell.org/read/functional-programming.html#fp.anonymous] 這部分,我們列舉了一系列不使用匿名函數(shù)的原因,然而在這里,我們將使用它作為函數(shù)體,這是匿名函數(shù)在 Haskell 中最常見(jiàn)的用途之一。

我們已經(jīng)在 froM 和 mapM 上看到使用函數(shù)作為參數(shù)的方式,許多循環(huán)體是程序中只出現(xiàn)一次的代碼塊。既然我們喜歡在循環(huán)中使用一個(gè)再也不會(huì)出現(xiàn)的循環(huán)體,那么為什么要給他們命名?

顯而易見(jiàn),有時(shí)候我們需要在不同的循環(huán)中嵌入相同的代碼,這時(shí)候我們不應(yīng)該使用匿名函數(shù),把他們剪貼和復(fù)制進(jìn)去,而是給這些匿名函數(shù)命名來(lái)調(diào)用,這樣顯得有意義一點(diǎn)

為什么提供 mapM 和 forM

存在兩個(gè)相同的函數(shù)看起來(lái)是有點(diǎn)奇怪,但接受參數(shù)的順序之間的差異使他們適用于不同的情況。

我們來(lái)考察下之前的例子,使用匿名函數(shù)作為循環(huán)體,如果我們使用 mapM 而非 forM ,我們將不得不把變量 properNames 放置到函數(shù)體的后邊,而為了讓代碼正確解析,我們就必須將整個(gè)匿名函數(shù)用括號(hào)包起來(lái),或者用一個(gè)不必要的命名函數(shù)將它取代,自己嘗試下,拷貝上邊的代碼,用 mapM 代替 forM ,觀察代碼可讀性上有什么變化

相反,如果循環(huán)體是一個(gè)命名函數(shù),而且我們要循環(huán)的列表是通過(guò)一個(gè)復(fù)雜表達(dá)式計(jì)算的,我們就找到了 mapM 的應(yīng)用場(chǎng)景

這里需要遵守的代碼風(fēng)格是無(wú)論通過(guò) mapM 和 forM 都讓你寫(xiě)出干凈的代碼,如果循環(huán)體或者循環(huán)中的表達(dá)式都很短,那么用哪個(gè)都無(wú)所謂,如果循環(huán)體很短,但數(shù)據(jù)很長(zhǎng),使用 mapM ,如果相反,則用 forM ,如果都很長(zhǎng),使用 let 或者 where 讓其中一個(gè)變短,通過(guò)這樣一些實(shí)踐,不同情況下那個(gè)實(shí)現(xiàn)最好就變得顯而易見(jiàn)

一個(gè)本地查找函數(shù)

我們可以使用 getRecursiveContents 函數(shù)作為一個(gè)內(nèi)置的簡(jiǎn)單文件查找器的基礎(chǔ)

-- file: ch09/SimpleFinder.hs
import RecursiveContents (getRecursiveContents)
simpleFind :: (FilePath -> Bool) -> FilePath -> IO [FilePath]
simpleFind p path = do
    names <- getRecursiveContents path
    return (filter p names)

上文的函數(shù)通過(guò)我們?cè)谶^(guò)濾器中的謂詞來(lái)匹配 getRecursiveContents 函數(shù)返回的名字,每個(gè)通過(guò)謂詞判斷的名稱都是文件全路徑,因此如何完成一個(gè)像“查找所有擴(kuò)展名以 .c 結(jié)尾的文件”的功能?

System.FilePath 模塊包含了許多有價(jià)值的函數(shù)來(lái)幫助我們操作文件名,在這個(gè)例子中,我們使用 takeExtension :

ghci> :m +System.FilePath
ghci> :type takeExtension
takeExtension :: FilePath -> String
ghci> takeExtension "foo/bar.c"
Loading package filepath-1.1.0.0 ... linking ... done.
".c"
ghci> takeExtension "quux"
""

下面的代碼給我們一個(gè)包括獲得路徑,獲得擴(kuò)展名,然后和.c進(jìn)行比較的簡(jiǎn)單功能的函數(shù)實(shí)現(xiàn),

ghci> :load SimpleFinder
[1 of 2] Compiling RecursiveContents ( RecursiveContents.hs, interpreted )
[2 of 2] Compiling Main             ( SimpleFinder.hs, interpreted )
Ok, modules loaded: RecursiveContents, Main.
ghci> :type simpleFind (\p -> takeExtension p == ".c")
simpleFind (\p -> takeExtension p == ".c") :: FilePath -> IO [FilePath]

simpleFind 在工作中有一些非常刺眼的問(wèn)題,第一個(gè)就是謂詞并不能準(zhǔn)確而完全的表達(dá),他只關(guān)注文件夾中的實(shí)體名稱,而無(wú)法做到辨認(rèn)這是個(gè)文件還是個(gè)目錄此類的事情,——而我們使用 simpleFind 的嘗試就是想列舉所文件和有和文件一樣擁有 .c 擴(kuò)展名的文件夾

第二個(gè)問(wèn)題是在 simpleFind 中我們無(wú)法控制它遍歷文件系統(tǒng)的方式,這是顯而易見(jiàn)的,想想在分布式版本控制系統(tǒng)中控制下的樹(shù)狀結(jié)構(gòu)中查找一個(gè)源文件的問(wèn)題吧,所有被控制的目錄都含有一個(gè) .svn 的私有文件夾,每一個(gè)包含了許多我們毫不感興趣的子文件夾和文件,簡(jiǎn)單的過(guò)濾所有包含 .svn 的路徑遠(yuǎn)比僅僅在讀取時(shí)避免遍歷這些文件夾更加有效。例如,一個(gè)分布式源碼樹(shù)包含了45000個(gè)文件,30000個(gè)分布在1200個(gè)不同的.svn文件夾中,避免遍歷這1200個(gè)文件夾比過(guò)濾他們包含的30000個(gè)文件代價(jià)更低。

最后。 simpleFind 是嚴(yán)格的,因?yàn)樗幌盗蠭O元操作執(zhí)行構(gòu)成的動(dòng)作,如果我們有一百萬(wàn)個(gè)文件要遍歷,我們需要等待很長(zhǎng)一段時(shí)間才能得到一個(gè)包含一百萬(wàn)個(gè)名字的巨大的返回值,這對(duì)用戶體驗(yàn)和資源消耗都是噩夢(mèng),我們更需要一個(gè)只有當(dāng)他們獲得結(jié)果的時(shí)才展示的結(jié)果流。

在接下來(lái)的環(huán)節(jié)里,我們將解決每個(gè)遇到的問(wèn)題

謂詞在保持純粹的同時(shí)支持從貧類型到富類型

我們的謂詞只關(guān)注文件名,這將一系列有趣的操作排除在外,試想下,假如我們希望列出比某個(gè)給定值更大的文件呢?

面對(duì)這個(gè)問(wèn)題的第一反應(yīng)是查找 IO :我們的謂詞是 FilePath->Bool 類型,為什么不把它變成 FilePath->IOBool 類型?這將使我們所有的IO操作都成為謂詞的一部分,但這在顯而易見(jiàn)的好處之外引入一個(gè)潛在的問(wèn)題,使用這樣一個(gè)謂詞存在各種可能的后果,比如一個(gè)有 IOa 類型返回的函數(shù)將有能力生成任何它想產(chǎn)生的結(jié)果。

讓我們?cè)陬愋拖到y(tǒng)中尋找以寫(xiě)出擁有更多謂詞,更少bug的代碼,我們通過(guò)避免污染IO來(lái)堅(jiān)持?jǐn)嘌缘募兇?,這將確保他們不會(huì)產(chǎn)生任何不純的結(jié)果,同時(shí)我們給他們提供更多信息,這樣他們就可以在不必誘發(fā)潛在的危險(xiǎn)的情況下獲得需要的表達(dá)式

Haskell 的 System.Directory 模塊提供了一個(gè)盡管受限但仍然有用的關(guān)于文件元數(shù)據(jù)的集合

ghci> :m +System.Directory

我們可以通過(guò) doesFileExist 和 doesDirectoryExist 來(lái)判斷目錄實(shí)體是目錄還是文件,但暫時(shí)還沒(méi)有更多方式來(lái)查找這些年里出現(xiàn)的紛繁復(fù)雜的其他文件類型,比如管道,硬鏈接和軟連接。

ghci> :type doesFileExist
doesFileExist :: FilePath -> IO Bool
ghci> doesFileExist "."
Loading package old-locale-1.0.0.0 ... linking ... done.
Loading package old-time-1.0.0.0 ... linking ... done.
Loading package directory-1.0.0.0 ... linking ... done.
False
ghci> :type doesDirectoryExist
doesDirectoryExist :: FilePath -> IO Bool
ghci> doesDirectoryExist "."
True

getPermissions 函數(shù)讓我們確定當(dāng)前對(duì)于文件或目錄的操作是否是合法:

ghci> :type getPermissions
getPermissions :: FilePath -> IO Permissions
ghci> :info Permissions
data Permissions
  = Permissions {readable :: Bool,
                 writable :: Bool,
                 executable :: Bool,
                 searchable :: Bool}
      -- Defined in System.Directory
instance Eq Permissions -- Defined in System.Directory
instance Ord Permissions -- Defined in System.Directory
instance Read Permissions -- Defined in System.Directory
instance Show Permissions -- Defined in System.Directory
ghci> getPermissions "."
Permissions {readable = True, writable = True, executable = False, searchable = True}
ghci> :type searchable
searchable :: Permissions -> Bool
ghci> searchable it
True

如果你無(wú)法回憶起 ghci 中變量 it 的特殊用法,回到第一章復(fù)習(xí)一下,如果我們的權(quán)限能夠列出它的內(nèi)容,那么這個(gè)目錄就應(yīng)該是可被搜索的,而文件則永遠(yuǎn)是不可搜索的

最后, getModificationTime 告訴我們實(shí)體上次被修改的時(shí)間:

ghci> :type getModificationTime
getModificationTime :: FilePath -> IO System.Time.ClockTime
ghci> getModificationTime "."
Mon Aug 18 12:08:24 CDT 2008

如果我們像標(biāo)準(zhǔn)的Haskell代碼一樣對(duì)可移植性要求嚴(yán)格,這些函數(shù)就是我們手頭所有的一切(我們同樣可以通過(guò)黑客手段來(lái)獲得文件大小),這些已經(jīng)足夠讓我們明白所感興趣領(lǐng)域中的原則,而非讓我們浪費(fèi)寶貴的時(shí)間對(duì)著一個(gè)例子冥思苦想,如果你需要寫(xiě)滿足更多需求的代碼, System.Posix 和 System.Win32 模塊提供關(guān)于當(dāng)代兩種計(jì)算平臺(tái)的更多文件元數(shù)據(jù)的細(xì)節(jié)。 Hackage 中同樣有一個(gè) unix-compat 包,提供windows下的類unix的api 。

新的富類型謂詞需要關(guān)注的數(shù)據(jù)段到底有幾個(gè)?自從我們可以通過(guò) Permissions 來(lái)判斷實(shí)體是文件還是目錄之后,我們就不再需要獲得 doesFileExist 和 doesDirectoryExist 的結(jié)果,因此一個(gè)謂詞需要關(guān)注的輸入有四個(gè)。

-- file: ch09/BetterPredicate.hs
import Control.Monad (filterM)
import System.Directory (Permissions(..), getModificationTime, getPermissions)
import System.Time (ClockTime(..))
import System.FilePath (takeExtension)
import Control.Exception (bracket, handle)
import System.IO (IOMode(..), hClose, hFileSize, openFile)

-- the function we wrote earlier
import RecursiveContents (getRecursiveContents)

type Predicate =  FilePath      -- path to directory entry
               -> Permissions   -- permissions
               -> Maybe Integer -- file size (Nothing if not file)
               -> ClockTime     -- last modified
               -> Bool

這一謂詞類型只是一個(gè)有四個(gè)參數(shù)的函數(shù)的同義詞,他將給我們節(jié)省一些鍵盤(pán)工作和屏幕空間。

注意這一返回值是 Bool 而非 IOBool ,謂詞需要保證純粹,而且不能表現(xiàn)IO,在擁有這種類型以后,我們的查找函數(shù)仍然顯得非??瞻?。

-- file: ch09/BetterPredicate.hs
-- soon to be defined
getFileSize :: FilePath -> IO (Maybe Integer)

betterFind :: Predicate -> FilePath -> IO [FilePath]

betterFind p path = getRecursiveContents path >>= filterM check
    where check name = do
            perms <- getPermissions name
            size <- getFileSize name
            modified <- getModificationTime name
            return (p name perms size modified)

先來(lái)閱讀代碼,由于隨后將討論 getFileSize 的某些細(xì)節(jié),因此現(xiàn)在暫時(shí)先跳過(guò)它。

我們無(wú)法使用 filter 來(lái)調(diào)用我們的謂詞,因?yàn)?p 的純粹代表他不能作為IO收集元數(shù)據(jù)的方式

這讓我們將目光轉(zhuǎn)移到一個(gè)并不熟悉的函數(shù) filterM 上,它的動(dòng)作就像普通的 filter 函數(shù),但在這種情況下,它在 IOmonad 操作中使用它的謂詞,進(jìn)而通過(guò)謂詞表現(xiàn)IO:

ghci> :m +Control.Monad
ghci> :type filterM
filterM :: (Monad m) => (a -> m Bool) -> [a] -> m [a]

check 謂詞是純謂詞 p 的IO功能包裝器,他執(zhí)行了所有IO發(fā)生在 p 上的可能引起負(fù)面效果的任務(wù),因此我們可以使 p 對(duì)負(fù)面效果免疫,在收集完元數(shù)據(jù)后, check 調(diào)用 p ,通過(guò) return 語(yǔ)句包裝 p 的IO返回結(jié)果

安全的獲得一個(gè)文件的大小

即使 System.Directory 不允許我們獲得一個(gè)文件的大小,我們?nèi)钥梢允褂?System.IO 的類似接口完成這項(xiàng)任務(wù),它包含了一個(gè)名為 hFileSize 的函數(shù),這一函數(shù)返回打開(kāi)文件的字節(jié)數(shù),下面是他的簡(jiǎn)單調(diào)用實(shí)例:

-- file: ch09/BetterPredicate.hs
simpleFileSize :: FilePath -> IO Integer

simpleFileSize path = do
  h <- openFile path ReadMode
  size <- hFileSize h
  hClose h
  return size

當(dāng)這個(gè)函數(shù)工作時(shí),他還不能完全為我們所用,在 betterFind 中,我們?cè)谀夸浵碌娜魏螌?shí)體上調(diào)用 getFileSize ,如果實(shí)體不是一個(gè)文件或者大小被 Just 包裝起來(lái),他應(yīng)當(dāng)返回一個(gè)空值,而當(dāng)實(shí)體不是文件或者沒(méi)有被打開(kāi)時(shí)(可能是由于權(quán)限不夠),這個(gè)函數(shù)會(huì)拋出一個(gè)異常然后返回一個(gè)未包裝的大小。

下文是安全的用法:

-- file: ch09/BetterPredicate.hs
saferFileSize :: FilePath -> IO (Maybe Integer)

saferFileSize path = handle (\_ -> return Nothing) $ do
  h <- openFile path ReadMode
  size <- hFileSize h
  hClose h
  return (Just size)

函數(shù)體幾乎完全一致,除了 handle 語(yǔ)句。

我們的異常捕捉在忽略通過(guò)的異常的同時(shí)返回一個(gè)空值,函數(shù)體唯一的變化就是允許通過(guò) Just 包裝文件大小

saferFileSize 函數(shù)現(xiàn)在有正確的類型簽名,并且不會(huì)拋出任何異常,但他扔未能完全的正常工作,存在 openFile 會(huì)成功的目錄實(shí)體,但 hFileSize 會(huì)拋出異常,這將和被稱作命名管道的狀況一起發(fā)生,這樣的異常會(huì)被捕捉,但卻從未發(fā)起調(diào)用 hClose 。

當(dāng)發(fā)現(xiàn)不再使用文件句柄,Haskell會(huì)自動(dòng)關(guān)閉它,但這只有在運(yùn)行垃圾回收時(shí)才會(huì)執(zhí)行,如果無(wú)法斷言,則延遲到下一次垃圾回收。

文件句柄是稀缺資源,稀缺性是通過(guò)操作系統(tǒng)強(qiáng)制保證的,在linux中,一個(gè)進(jìn)程只能同時(shí)擁有1024個(gè)文件句柄。

不難想象這種場(chǎng)景,程序調(diào)用了一個(gè)使用 saferFileSize 的 betterFind 函數(shù),在足夠的垃圾文件句柄被關(guān)閉之前,由于 betterFind 造成文件句柄數(shù)耗盡導(dǎo)致了程序崩潰

這是bug危害性的一方面:通過(guò)合并起來(lái)的不同的部分使得bug不易被排查,只有在 betterFind 訪問(wèn)足夠多的非文件達(dá)到進(jìn)程打開(kāi)文件句柄數(shù)上限的時(shí)候才會(huì)被觸發(fā),隨后在積累的垃圾文件句柄被關(guān)閉之前返回一個(gè)嘗試打開(kāi)另一個(gè)文件的調(diào)用。

任何程序內(nèi)由無(wú)法獲得數(shù)據(jù)造成的后續(xù)錯(cuò)誤都會(huì)讓事情變得更糟,直到垃圾回收為止。修正這樣一個(gè)bug需要程序結(jié)構(gòu)本身支持,文件系統(tǒng)內(nèi)容,如何關(guān)閉當(dāng)前正在運(yùn)行的程序以觸發(fā)垃圾回收

這種問(wèn)題在開(kāi)發(fā)中很容易被檢查出來(lái),然而當(dāng)他在上線之后出現(xiàn)(這些惡心的問(wèn)題一向如此),就變得非常難以發(fā)覺(jué)

幸運(yùn)的是,我們可以很容易避開(kāi)這種錯(cuò)誤,同時(shí)又能縮短我們的函數(shù)。

請(qǐng)求-使用-釋放循環(huán)

每當(dāng) openFile 成功之后我們就必須保證調(diào)用 hClose , Control.Exception 模塊提供了 bracket 函數(shù)來(lái)支持這個(gè)想法:

ghci> :type bracket
bracket :: IO a -> (a -> IO b) -> (a -> IO c) -> IO c

bracket 函數(shù)需要三個(gè)動(dòng)作作為參數(shù),第一個(gè)動(dòng)作需要一個(gè)資源,第二個(gè)動(dòng)作釋放這個(gè)資源,第三個(gè)動(dòng)作在這兩個(gè)中執(zhí)行,當(dāng)資源被請(qǐng)求,我們稱他為操作動(dòng)作,當(dāng)請(qǐng)求動(dòng)作成功,釋放動(dòng)作隨后總是被調(diào)用,這保證了這個(gè)資源一直能夠被釋放,對(duì)通過(guò)的每個(gè)請(qǐng)求資源文件的操作,使用和釋放動(dòng)作都是必要的。

如果一個(gè)異常發(fā)生在使用過(guò)程中, bracket 調(diào)用釋放動(dòng)作并拋出異常,如果使用動(dòng)作成功, bracket 調(diào)用釋放動(dòng)作,同時(shí)返回使用動(dòng)作返回的值。

我們現(xiàn)在可以寫(xiě)一個(gè)完全安全的函數(shù)了,他將不會(huì)拋出異常,也不會(huì)積累可能在我們程序其他地方制造失敗的垃圾文件句柄數(shù)。

-- file: ch09/BetterPredicate.hs
getFileSize path = handle (\_ -> return Nothing) $
  bracket (openFile path ReadMode) hClose $ \h -> do
    size <- hFileSize h
    return (Just size)

仔細(xì)觀察 bracket 的參數(shù),首先打開(kāi)文件,并且返回文件句柄,第二步關(guān)閉句柄,第三步在句柄上調(diào)用 hFileSize 并用 just 包裝結(jié)果返回

為了這個(gè)函數(shù)的正常工作,我們需要使用 bracket 和 handle ,前者保證我們不會(huì)積累垃圾文件句柄數(shù),后者保證我們免于異常。

練習(xí)

  1. 調(diào)用 bracket 和 handle 的順序重要嗎,為什么

為謂詞而開(kāi)發(fā)的領(lǐng)域特定語(yǔ)言

深入謂詞寫(xiě)作的內(nèi)部,我們的謂詞將檢查大于128kb的C++源文件:

-- file: ch09/BetterPredicate.hs
myTest path _ (Just size) _ =
    takeExtension path == ".cpp" && size > 131072
myTest _ _ _ _ = False

這并不是令人感到愉快的工作,斷言需要四個(gè)參數(shù),并且總是忽略其中的兩個(gè),同時(shí)需要定義兩個(gè)等式,寫(xiě)一些更有意義的謂詞代碼,我們可以做的更好。

有些時(shí)候,這種庫(kù)被用作嵌入式領(lǐng)域特定語(yǔ)言,我們通過(guò)編寫(xiě)代碼的過(guò)程中通過(guò)編程語(yǔ)言的本地特性來(lái)優(yōu)雅的解決一些特定問(wèn)題

第一步是寫(xiě)一個(gè)返回當(dāng)前函數(shù)的一個(gè)參數(shù)的函數(shù),這個(gè)從參數(shù)中抽取路徑并傳給謂詞:

-- file: ch09/BetterPredicate.hs
pathP path _ _ _ = path

如果我們不能提供類型簽名, Haskell 將給這個(gè)函數(shù)提供一個(gè)通用類型,這在隨后會(huì)導(dǎo)致一個(gè)難以理解的錯(cuò)誤信息,因此給 pathP 一個(gè)類型:

-- file: ch09/BetterPredicate.hs
type InfoP a =  FilePath        -- path to directory entry
             -> Permissions     -- permissions
             -> Maybe Integer   -- file size (Nothing if not file)
             -> ClockTime       -- last modified
             -> a

pathP :: InfoP FilePath

我們已經(jīng)創(chuàng)建了一個(gè)可以用做縮寫(xiě)的類型,相似的結(jié)構(gòu)函數(shù),我們的類型代詞接受一個(gè)類型參數(shù),如此我們可以分辨不同的結(jié)果類型:

-- file: ch09/BetterPredicate.hs
sizeP :: InfoP Integer
sizeP _ _ (Just size) _ = size
sizeP _ _ Nothing     _ = -1

我們?cè)谶@里做了些小動(dòng)作,對(duì)那些我們無(wú)法打開(kāi)的文件或者不是文件的東西我們返回的實(shí)體大小是 -1 。

事實(shí)上,瀏覽中可以看出我們?cè)诒菊麻_(kāi)始處定義謂詞類型的和 InfoPBool 一樣,因此我們可以合法的放棄謂詞類型。

pathP 和 sizeP 的用法?通過(guò)一些線索,我們發(fā)現(xiàn)可以在一個(gè)謂詞中使用它們(每個(gè)名稱中的前綴p代表斷言),從這開(kāi)始事情就變得有趣起來(lái):

-- file: ch09/BetterPredicate.hs
equalP :: (Eq a) => InfoP a -> a -> InfoP Bool
equalP f k = \w x y z -> f w x y z == k

equalP 的類型簽名值得注意,他接受一個(gè) InfoPa ,同時(shí)兼容 pathP 和 sizeP ,他接受一個(gè) a ,并返回一個(gè)被認(rèn)為是謂詞同義詞的 InfoPBool ,換言之, equalP 構(gòu)造了一個(gè)謂詞。

equalP 函數(shù)通過(guò)返回一個(gè)匿名函數(shù)工作,謂詞接受參數(shù)之后將他們轉(zhuǎn)成 f ,并將結(jié)果和 f 進(jìn)行比對(duì)。

equalP 的相等強(qiáng)調(diào)了這一事實(shí),我們認(rèn)為它需要兩個(gè)參數(shù),在 Haskell 柯里化處理了所有函數(shù)的情況下,通過(guò)這種方式寫(xiě) equalP 并無(wú)必要,我們可以避免匿名函數(shù),同時(shí)通過(guò)柯里化來(lái)寫(xiě)出表現(xiàn)相同的函數(shù):

-- file: ch09/BetterPredicate.hs
equalP' :: (Eq a) => InfoP a -> a -> InfoP Bool
equalP' f k w x y z = f w x y z == k

在繼續(xù)我們的探險(xiǎn)之前,先把寫(xiě)好的模塊加載到 ghci 里去:

ghci> :load BetterPredicate
[1 of 2] Compiling RecursiveContents ( RecursiveContents.hs, interpreted )
[2 of 2] Compiling Main             ( BetterPredicate.hs, interpreted )
Ok, modules loaded: RecursiveContents, Main.

讓我們來(lái)看看函數(shù)中的簡(jiǎn)單謂詞能否正常工作:

ghci> :type betterFind (sizeP `equalP` 1024)
betterFind (sizeP `equalP` 1024) :: FilePath -> IO [FilePath]

注意我們并沒(méi)有直接調(diào)用 betterFind ,我們只是確定我們的表達(dá)式進(jìn)行了類型檢查,現(xiàn)在我們需要更多的方法來(lái)列出大小為特定值的所有文件,之前的成功給了我們繼續(xù)下去的勇氣。

多用提升(lifting)少用模板

除了 equalP ,我們還將能夠編寫(xiě)其他二進(jìn)制函數(shù),我們更希望不去寫(xiě)他們每個(gè)的具體實(shí)現(xiàn),因?yàn)檫@看起來(lái)只是重復(fù)工作:

-- file: ch09/BetterPredicate.hs
liftP :: (a -> b -> c) -> InfoP a -> b -> InfoP c
liftP q f k w x y z = f w x y z `q` k

greaterP, lesserP :: (Ord a) => InfoP a -> a -> InfoP Bool
greaterP = liftP (>)
lesserP = liftP (<)

為了完成這個(gè),讓我們使用 Haskell 的抽象功能,定義 equalP 代替直接調(diào)用 == ,我們就可以把二進(jìn)制函數(shù)作為參數(shù)傳入我們想調(diào)用的函數(shù)。

函數(shù)動(dòng)作,比如 > ,以及將它轉(zhuǎn)換成另一個(gè)函數(shù)操作另一種上下文,在這里是 greaterP ,通過(guò)提升(lifting)將它引入到上下文,這解釋了當(dāng)前函數(shù)名稱中l(wèi)ifting出現(xiàn)的原因,提升讓我們復(fù)用代碼并降低模板的使用,在本書(shū)的后半部分的內(nèi)容中,我們將大量使用這一技術(shù)

當(dāng)我們提升一個(gè)函數(shù),我們通常將它轉(zhuǎn)換到原始類型和一個(gè)新版本——提升和未提升兩個(gè)版本

在這里,將 q 作為 liftP 的第一個(gè)參數(shù)是經(jīng)過(guò)深思熟慮的,這使得我們可能寫(xiě)一個(gè)對(duì) greaterP 和 lesserP 都有意義的定義,實(shí)踐中發(fā)現(xiàn),相較其他語(yǔ)言,Haskell 中參數(shù)的最佳適配成為api設(shè)計(jì)中最重要的一部分。語(yǔ)言內(nèi)部要求參數(shù)轉(zhuǎn)換,在Haskell中放錯(cuò)一個(gè)參數(shù)的位置就將失去程序的所有意義。

我們可以通過(guò)組合字(combinators)恢復(fù)一些意義,比如,直到2007年 forM 才加入 Control.Monad 模塊,在此之前,人們用的是 flipmapM 。

ghci> :m +Control.Monad
ghci> :t mapM
mapM :: (Monad m) => (a -> m b) -> [a] -> m [b]
ghci> :t forM
forM :: (Monad m) => [a] -> (a -> m b) -> m [b]
ghci> :t flip mapM
flip mapM :: (Monad m) => [a] -> (a -> m b) -> m [b]

謂詞組合

如果我們希望組合謂詞,我們可以循著手邊最明顯的路徑來(lái)開(kāi)始

-- file: ch09/BetterPredicate.hs
simpleAndP :: InfoP Bool -> InfoP Bool -> InfoP Bool
simpleAndP f g w x y z = f w x y z && g w x y z

現(xiàn)在我們知道了提升,他成為通過(guò)提升存在的布爾操作來(lái)削減代碼量的更自然的選擇。

-- file: ch09/BetterPredicate.hs
liftP2 :: (a -> b -> c) -> InfoP a -> InfoP b -> InfoP c
liftP2 q f g w x y z = f w x y z `q` g w x y z

andP = liftP2 (&&)
orP = liftP2 (||)

注意 liftP2 非常像我們之前的 liftP ,事實(shí)上,這更加通用,因?yàn)槲覀兛梢杂?liftP 代替 liftP2 :

-- file: ch09/BetterPredicate.hs
constP :: a -> InfoP a
constP k _ _ _ _ = k

liftP' q f k w x y z = f w x y z `q` constP k w x y z

Note

組合子

在Haskell中,我們更希望函數(shù)的傳入?yún)?shù)和返回值都是函數(shù),就像組合子一樣

回到之前定義的 myTest 函數(shù),現(xiàn)在我們可以使用一些幫助函數(shù)了。

-- file: ch09/BetterPredicate.hs
myTest path _ (Just size) _ =
    takeExtension path == ".cpp" && size > 131072
myTest _ _ _ _ = False

在加入組合字以后這個(gè)函數(shù)會(huì)變成什么樣子:

-- file: ch09/BetterPredicate.hs
liftPath :: (FilePath -> a) -> InfoP a
liftPath f w _ _ _ = f w

myTest2 = (liftPath takeExtension `equalP` ".cpp") `andP`
          (sizeP `greaterP` 131072)

由于操作文件名是如此平常的行為,我們加入了最終組合字 liftPath 。

定義并使用新算符

可以通過(guò)特定領(lǐng)域語(yǔ)言定義新的操作:

-- file: ch09/BetterPredicate.hs
(==?) = equalP
(&&?) = andP
(>?) = greaterP

myTest3 = (liftPath takeExtension ==? ".cpp") &&? (sizeP >? 131072)

這個(gè)括號(hào)在定義中是必要的,因?yàn)椴⑽锤嬖VHaskell有關(guān)之前和相關(guān)的操作,領(lǐng)域語(yǔ)言的操作如果沒(méi)有邊界(fixities)聲明將會(huì)被以 infixl9 之類的東西對(duì)待,計(jì)算從左到右,如果跳過(guò)這個(gè)括號(hào),表達(dá)式將被解析成具有可怕錯(cuò)誤的 (((liftPathtakeExtension)==?".cpp")&&?sizeP)>?131072 。

可以給操作添加邊界聲明,第一步是找出未提升的操作的,這樣就可以模仿他們了:

ghci> :info ==
class Eq a where
  (==) :: a -> a -> Bool
  ...
      -- Defined in GHC.Base
infix 4 ==
ghci> :info &&
(&&) :: Bool -> Bool -> Bool        -- Defined in GHC.Base
infixr 3 &&
ghci> :info >
class (Eq a) => Ord a where
  ...
  (>) :: a -> a -> Bool
  ...
    -- Defined in GHC.Base
infix 4 >

學(xué)會(huì)這些就可以寫(xiě)一個(gè)不用括號(hào)的表達(dá)式,卻和 myTest3 的解析結(jié)果一致的表達(dá)式了

控制遍歷

遍歷文件系統(tǒng)時(shí),我們喜歡在需要遍歷的文件夾上有更多的控制權(quán),簡(jiǎn)便方法之一是可以在函數(shù)中允許給定文件夾的部分子文件夾通過(guò),然后返回另一個(gè)列表,這個(gè)列表可以移除元素,也可以要求和原始列表不同,或兩者皆有,最簡(jiǎn)單的控制函數(shù)就是id,原樣返回未修改的列表。

為了應(yīng)付多種情況,我們正在嘗試改變部分表達(dá),為了替代精心刻畫(huà)的函數(shù)類型 InfoP ,我們將使用一個(gè)普通代數(shù)數(shù)據(jù)類型來(lái)表達(dá)相同的含義

-- file: ch09/ControlledVisit.hs
data Info = Info {
      infoPath :: FilePath
    , infoPerms :: Maybe Permissions
    , infoSize :: Maybe Integer
    , infoModTime :: Maybe ClockTime
    } deriving (Eq, Ord, Show)

getInfo :: FilePath -> IO Info

記錄語(yǔ)法給我們自由控制函數(shù)的權(quán)限,如 infoPath , traverse 函數(shù)中的這種類型是簡(jiǎn)單地,正如我們之前期望的那樣,如果需要一個(gè)文件或者目錄的信息,就調(diào)用 getInfo 函數(shù):

-- file: ch09/ControlledVisit.hs
traverse :: ([Info] -> [Info]) -> FilePath -> IO [Info]

traverse 的定義很短,但很有分量:

-- file: ch09/ControlledVisit.hs
traverse order path = do
    names <- getUsefulContents path
    contents <- mapM getInfo (path : map (path </>) names)
    liftM concat $ forM (order contents) $ \info -> do
      if isDirectory info && infoPath info /= path
        then traverse order (infoPath info)
        else return [info]

getUsefulContents :: FilePath -> IO [String]
getUsefulContents path = do
    names <- getDirectoryContents path
    return (filter (`notElem` [".", ".."]) names)

isDirectory :: Info -> Bool
isDirectory = maybe False searchable . infoPerms

現(xiàn)在不再引入新技術(shù),這就是我們遇到的最深?yuàn)W的函數(shù)定義,一行行的深入他,解釋它每行為何是這樣,不過(guò)開(kāi)始部分的那幾行沒(méi)什么神秘的,它們只是之前看到代碼的拷貝。

觀察變量 contents 的時(shí)候情況變得有趣起來(lái),從左到右仔細(xì)閱讀,已經(jīng)知道 names 是目錄實(shí)體的列表,同時(shí)確定當(dāng)前目錄的所有元素都在這個(gè)列表中,這時(shí)通過(guò) mapM 將 getInfo 附加到結(jié)果返回的路徑上。

接下來(lái)的這一行更深?yuàn)W,繼續(xù)從左往右看,我們看到本行的最后一個(gè)元素以一個(gè)匿名函數(shù)的定義開(kāi)始,并持續(xù)到這一段的結(jié)尾,給定一個(gè)Info值,函數(shù)或者遞歸訪問(wèn)一個(gè)目錄(有額外的方法保證我們不在訪問(wèn)這個(gè)路徑),或者返回當(dāng)前值作為列表唯一元素的列表(來(lái)匹配遞歸的返回類型)。

函數(shù)通過(guò) forM 獲得 order 返回 info 列表中的每個(gè)元素, forM 是使用者提供的遞歸控制函數(shù)。

本行的新上下文中使用提升技術(shù), liftM 函數(shù)需要一個(gè)規(guī)則函數(shù), concat ,并且提升到 IO 的monad操作,換言之,他需要 forM 通過(guò) IO monad 操作的的返回值,并將 concat 附加其上(獲得一個(gè) [Info] 類型的返回值,這也是我們所需要的)并將結(jié)果值返回給 IO monad 。

最后不要忘記定義 getInfo 函數(shù):

-- file: ch09/ControlledVisit.hs
maybeIO :: IO a -> IO (Maybe a)
maybeIO act = handle (\_ -> return Nothing) (Just `liftM` act)

getInfo path = do
  perms <- maybeIO (getPermissions path)
  size <- maybeIO (bracket (openFile path ReadMode) hClose hFileSize)
  modified <- maybeIO (getModificationTime path)
  return (Info path perms size modified)

在此唯一值得記錄的事情是一個(gè)有用的組合字, maybeIO ,將一個(gè)可能拋出異常的 IO 操作轉(zhuǎn)換成用 Maybe 包裝的結(jié)果

練習(xí)

  1. 在以代數(shù)順序遍歷一個(gè)目錄樹(shù)時(shí)如何確定需要通過(guò)的內(nèi)容。
  2. 使用 id 作為控制函數(shù), traverseid 扮演一個(gè)前序遞歸樹(shù),在子目錄之前他返回一個(gè)父目錄,寫(xiě)一個(gè)控制函數(shù)讓 traverse 表現(xiàn)為一個(gè)后序遍歷,返回子目錄在父目錄之前。
  3. 使得《謂詞組合》一節(jié)里面的斷言和組合字可以處理新的 info 類型。
  4. 給 traverse 寫(xiě)一個(gè)包裝器,讓你通過(guò)謂詞控制遞歸,并通過(guò)謂詞過(guò)濾返回結(jié)果

代碼深度,可讀性和學(xué)習(xí)過(guò)程

traverse 這樣深度的代碼在Haskell中并不多見(jiàn),在這種表達(dá)方式中里學(xué)習(xí)的收獲是巨大的,同時(shí)也并不需要大量的練習(xí)才能以這種方式流利的閱讀和寫(xiě)作代碼:

-- file: ch09/ControlledVisit.hs
traverseVerbose order path = do
    names <- getDirectoryContents path
    let usefulNames = filter (`notElem` [".", ".."]) names
    contents <- mapM getEntryName ("" : usefulNames)
    recursiveContents <- mapM recurse (order contents)
    return (concat recursiveContents)
  where getEntryName name = getInfo (path </> name)
        isDirectory info = case infoPerms info of
                             Nothing -> False
                             Just perms -> searchable perms
        recurse info = do
            if isDirectory info && infoPath info /= path
                then traverseVerbose order (infoPath info)
                else return [info]

作為對(duì)比,這里有一個(gè)不那么復(fù)雜的代碼,這也許適合一個(gè)對(duì)Haskell了解不那么深入的程序員

這里所做的一切都是創(chuàng)建一個(gè)新的替代,通過(guò)部分應(yīng)用(partial application)和函數(shù)組合(function composition)替代liberally,在 where 塊中我們已經(jīng)定義了一些本地函數(shù),在 maybe 組合子中,使用了 case 表達(dá)式,為了替代 liftM ,我們手動(dòng)將 concat 提升。

并不是說(shuō)深度是一個(gè)不好的特征, traverse 函數(shù)的每一行原始代碼都很短,我們引入一個(gè)本地變量和本地函數(shù)來(lái)保證代碼干凈且足夠短,命名注意可讀性,同時(shí)使用函數(shù)組合管道,最長(zhǎng)的管道只含有三個(gè)元素。

編寫(xiě)可維護(hù)的Haskell代碼核心是找到深度和可讀性的折中,能否做到這點(diǎn)取決于你的實(shí)踐層次:

  • 成為Haskell程序員之前,Andrew并不知道使用標(biāo)準(zhǔn)庫(kù)的方式,為此付出的代價(jià)則是寫(xiě)了一大堆不必要的重復(fù)代碼。
  • Zack是一個(gè)有數(shù)月編程經(jīng)驗(yàn)的,并且精通通過(guò)( . )組合長(zhǎng)管道的技巧。每當(dāng)代碼需要改動(dòng),就需要重構(gòu)一個(gè)管道,他無(wú)法更深入的理解已經(jīng)存在的管道的意義,而這些管道也太脆弱而無(wú)法修正。
  • Monica有相當(dāng)時(shí)間的編程經(jīng)驗(yàn),他對(duì)Haskell庫(kù)和編寫(xiě)整潔的代碼非常熟悉,但他避免使用高深度的風(fēng)格,她的代碼可維護(hù),同時(shí)她還找到了一種簡(jiǎn)單地方法來(lái)面對(duì)快速的需求變更

觀察迭代函數(shù)的另一種方法

相比原始的 betterFind 函數(shù),迭代函數(shù)給我們更多控制權(quán)的同時(shí)仍存在一個(gè)問(wèn)題,我們可以避免遞歸目錄,但我們不能過(guò)濾其他文件名直到我們獲得整個(gè)名稱樹(shù),如果遞歸含有100000個(gè)文件的目錄的同時(shí)只關(guān)注其中三個(gè),在獲得這三個(gè)需要的文件名之前需要給出一個(gè)含有10000個(gè)元素的表。

一個(gè)可能的方法是提供一個(gè)過(guò)濾器作為遞歸的新參數(shù),我們將它應(yīng)用到生成的名單中,這將允許我們獲得一個(gè)只包含我們需要元素的列表

然而,這個(gè)方法也存在缺點(diǎn),假如說(shuō)我們知道需要比三個(gè)多很多的實(shí)體組,并且這些實(shí)體組是這10000個(gè)我們需要遍歷實(shí)體中的前幾個(gè),這種情況下就不需要訪問(wèn)剩下的實(shí)體,這并不是個(gè)故弄玄虛的問(wèn)題,舉個(gè)栗子,郵箱文件夾中存放了包含許多郵件信息的文件夾——就像一個(gè)有大量文件的目錄,那么代表郵箱的目錄含有數(shù)千個(gè)文件就很正常。

從另一個(gè)角度看,我們嘗試定位之前兩個(gè)遍歷函數(shù)的弱點(diǎn):我們?nèi)绾慰创募到y(tǒng)遍歷階級(jí)目錄下的一個(gè)文件夾?

相似的文件夾, foldr 和 foldl' ,干凈的生成遍歷并計(jì)算出一個(gè)結(jié)果,很難把這個(gè)想法從列表擴(kuò)展到目錄樹(shù),但我們?nèi)詷?lè)于在 fold 中加入一個(gè)控制元素,我們將這個(gè)控制表達(dá)為一個(gè)代數(shù)數(shù)據(jù)類型:

-- file: ch09/FoldDir.hs
data Iterate seed = Done     { unwrap :: seed }
                  | Skip     { unwrap :: seed }
                  | Continue { unwrap :: seed }
                    deriving (Show)

type Iterator seed = seed -> Info -> Iterate seed

Iterator 類型給函數(shù)一個(gè)便于使用的別名,它需要一個(gè)種子和一個(gè) info 值來(lái)表達(dá)這個(gè)目錄實(shí)體,并返回一個(gè)新種子和一個(gè)我們 fold 函數(shù)的說(shuō)明,這個(gè)說(shuō)明通過(guò) Iterate 類型的構(gòu)造器來(lái)表達(dá):

  • 如果這個(gè)構(gòu)造器已經(jīng)完成,遍歷將立即釋放,被 Done 包裹的值將作為結(jié)果返回。
  • 如果這個(gè)說(shuō)明被跳過(guò),并且當(dāng)前 info 代表一個(gè)目錄,遍歷將不在遞歸尋找這個(gè)目錄。
  • 其他,這個(gè)便利仍將繼續(xù),使用包裹值作為下一個(gè)調(diào)用 fold 函數(shù)的參數(shù)。

目錄邏輯上是左序的,因?yàn)槲覀冮_(kāi)始從我們第一個(gè)遇到的實(shí)體開(kāi)始 fold 操作,而每步中的種子是之前一步的結(jié)果。

-- file: ch09/FoldDir.hs
foldTree :: Iterator a -> a -> FilePath -> IO a

foldTree iter initSeed path = do
    endSeed <- fold initSeed path
    return (unwrap endSeed)
  where
    fold seed subpath = getUsefulContents subpath >>= walk seed

    walk seed (name:names) = do
      let path' = path </> name
      info <- getInfo path'
      case iter seed info of
        done@(Done _) -> return done
        Skip seed'    -> walk seed' names
        Continue seed'
          | isDirectory info -> do
              next <- fold seed' path'
              case next of
                done@(Done _) -> return done
                seed''        -> walk (unwrap seed'') names
          | otherwise -> walk seed' names
    walk seed _ = return (Continue seed)

這部分代碼中有意思的部分很少,開(kāi)始是通過(guò) scoping 避免通過(guò)額外的參數(shù),最高層 foldTree 函數(shù)只是 fold 的包裝器,用來(lái)揭開(kāi) fold 的最后結(jié)果的生成器。

由于 fold 是本地函數(shù),我們不需要通過(guò) foldTree 的 iter 變量來(lái)進(jìn)入他,可以從外部進(jìn)入,相似的, walk 也可以在外部看到 path 。

另一個(gè)需要指出的點(diǎn)是 walk 是一個(gè)尾遞歸,在我們最初的函數(shù)中用來(lái)替代一個(gè)匿名函數(shù)調(diào)用。通過(guò)外部控制,可以在任何需要的時(shí)候停止,這使得當(dāng) iterator 返回 Done 的時(shí)候就可以退出。

即使 fold 調(diào)用 walk , walk 調(diào)用 fold 這樣的遞歸來(lái)遍歷子目錄,每個(gè)函數(shù)返回一個(gè)用 Iterate 包裝起來(lái)的種子,當(dāng) fold 被調(diào)用,并且返回, walk 檢查返回并觀察需要繼續(xù)還是退出,通過(guò)這種方式,一個(gè) Done 的返回直接終止兩個(gè)函數(shù)中的所有遞歸調(diào)用。

實(shí)踐中一個(gè) iterator 像什么,下面是一個(gè)觀察三個(gè)位圖文件(至多)的同時(shí)并不逆向遞歸元數(shù)據(jù)目錄的復(fù)雜例子:

-- file: ch09/FoldDir.hs
atMostThreePictures :: Iterator [FilePath]

atMostThreePictures paths info
    | length paths == 3
      = Done paths
    | isDirectory info && takeFileName path == ".svn"
      = Skip paths
    | extension `elem` [".jpg", ".png"]
      = Continue (path : paths)
    | otherwise
      = Continue paths
  where extension = map toLower (takeExtension path)
        path = infoPath info

為了使用這個(gè)需要調(diào)用 foldTreeatMostThreePictures[] ,它給我們一個(gè) IO[FilePath] 類型的返回值。

當(dāng)然, iterators 并不需要如此復(fù)雜,下面是個(gè)對(duì)目錄進(jìn)行計(jì)數(shù)的代碼:

-- file: ch09/FoldDir.hs
countDirectories count info =
    Continue (if isDirectory info
              then count + 1
              else count)

傳遞給 foldTree 的初始種子(seed)為數(shù)字零。

練習(xí)

  1. 修正 foldTree 來(lái)允許調(diào)用改變遍歷目錄實(shí)體的順序。
  2. foldTree 函數(shù)展示了前序遍歷,將它修正為允許調(diào)用方?jīng)Q定便利順序。
  3. 寫(xiě)一個(gè)組合子的庫(kù)允許 foldTree 接收不同類型的 iterators ,你能寫(xiě)出更簡(jiǎn)潔的 iterators 嗎?

代碼指南

有許多好的Haskell程序員的習(xí)慣來(lái)自經(jīng)驗(yàn),我們有一些通用的經(jīng)驗(yàn)給你,這樣你可以更快的寫(xiě)出易于閱讀的代碼。

正如已經(jīng)提到的,Haskell中永遠(yuǎn)使用空格,而不是tab 。

如果你發(fā)現(xiàn)代碼里有個(gè)片段聰明到炸裂,停下來(lái),然后思考下如果你離開(kāi)代碼一個(gè)月是否還能懂這段代碼。

常規(guī)命名類型和變量一般是駱駝法,例如 myVariableName ,這種風(fēng)格在Haskell中也同樣流行,不要去想你的其他命名習(xí)慣,如果你遵循一個(gè)不標(biāo)準(zhǔn)的慣例,那么你的代碼將會(huì)對(duì)其他人的眼睛造成折磨。

即使你已經(jīng)用了Haskell一段時(shí)間,在你寫(xiě)小函數(shù)之前花費(fèi)幾分鐘的時(shí)間查閱庫(kù)函數(shù),如果標(biāo)準(zhǔn)庫(kù)并沒(méi)有提供你需要的函數(shù),你可能需要組合出一個(gè)新的函數(shù)來(lái)獲得你想要的結(jié)果。

組合函數(shù)的長(zhǎng)管道難以閱讀,長(zhǎng)意味著包含三個(gè)以上元素的序列,如果你有這樣一個(gè)管道,使用 let 或者 where 語(yǔ)句塊將它分解成若干個(gè)小部分,給每個(gè)管道元素一個(gè)有意義的名字,然后再將他們回填到代碼,如果你想不出一個(gè)有意義的名字,問(wèn)下自己 能不能解釋這段代碼的功能,如果不能,簡(jiǎn)化你的代碼。

即使在編輯器中很容易格式化長(zhǎng)于八十列的代碼,寬度仍然是個(gè)重要問(wèn)題,寬行在80行之外的內(nèi)容通常會(huì)被截?cái)?,這非常傷害可讀性,每一行不超過(guò)八十個(gè)字符,這樣你就可以寫(xiě)入單獨(dú)的一行,這幫助你保持每一行代碼不那么復(fù)雜,從而更容易被人讀懂。

常用布局風(fēng)格

只要你的代碼遵守布局規(guī)范,那么他并不會(huì)給人一團(tuán)亂麻的感覺(jué),因此也不會(huì)造成誤解,也就是說(shuō),有些布局風(fēng)格是常用的。

in 關(guān)鍵字通常正對(duì)著 let 關(guān)鍵字,如下所示:

-- file: ch09/Style.hs
tidyLet = let foo = undefinedwei's
              bar = foo * 2
          in undefined

單獨(dú)列出 in 或者讓 in 在一系列等式之后跟著的寫(xiě)法都是正確的,但下面這種寫(xiě)法則會(huì)顯得很奇怪:

-- file: ch09/Style.hs
weirdLet = let foo = undefined
               bar = foo * 2
    in undefined

strangeLet = let foo = undefined
                 bar = foo * 2 in
    undefined

與此相反,讓 do 在行尾跟著而非在行首單獨(dú)列出:

-- file: ch09/Style.hs
commonDo = do
  something <- undefined
  return ()

-- not seen very often
rareDo =
  do something <- undefined
     return ()

括號(hào)和分號(hào)即使合法也很少用到,他們的使用并不存在問(wèn)題,只是讓代碼看起來(lái)奇怪,同時(shí)讓Haskell寫(xiě)成的代碼不必遵守排版規(guī)則。

-- file: ch09/Style.hs
unusualPunctuation =
    [ (x,y) | x <- [1..a], y <- [1..b] ] where {
                                           b = 7;
 a = 6 }

preferredLayout = [ (x,y) | x <- [1..a], y <- [1..b] ]
    where b = 7
          a = 6

如果等式的右側(cè)另起一行,通常在和他本行內(nèi),相關(guān)變量名或者函數(shù)定義的下方之前留出一些空格。

-- file: ch09/Style.hs
normalIndent =
    undefined

strangeIndent =
                           undefined

空格縮進(jìn)的數(shù)量有多種選擇,有時(shí)候在一個(gè)文件中,二,三,四格縮進(jìn)都很正常,一個(gè)縮進(jìn)也合法,但不常用,而且容易被誤讀。

寫(xiě) where 語(yǔ)句的縮進(jìn)時(shí),最好讓它分辨起來(lái)比較容易:

-- file: ch09/Style.hs
goodWhere = take 5 lambdas
    where lambdas = []

alsoGood =
    take 5 lambdas
  where
    lambdas = []

badWhere =           -- legal, but ugly and hard to read
    take 5 lambdas
    where
    lambdas = []

練習(xí)

即使本章內(nèi)容指導(dǎo)你們完成文件查找代碼,但這并不意味著真正的系統(tǒng)編程,因?yàn)閔askell移植的 IO 庫(kù)并不暴露足夠的消息給我們寫(xiě)有趣和復(fù)雜的查詢。

  1. 把本章代碼移植到你使用平臺(tái)的 api 上, System.Posix 或者 System.Win32 。
  2. 加入查找文件所有者的功能,將這個(gè)屬性對(duì)謂詞可見(jiàn)。
以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)