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

2018-02-24 15:49 更新

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

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

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

find命令

如果你不曾用過類Unix的系統(tǒng),或者你不是個(gè)重度shell用戶,那么你很可能從未聽說過 find ,通過給定的一組目錄,它遞歸搜索每個(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á)式,通過and或or算子就可以把他們裝配起來構(gòu)成更加復(fù)雜的表達(dá)式

簡單的開始:遞歸遍歷目錄

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

filter 表達(dá)式確保一個(gè)目錄的列表不含特定的目錄名(比如代表當(dāng)前目錄的 . 和上一級(jí)目錄的 .. ),如果忘記過濾這些,隨后的查找將陷入無限循環(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 中有特殊的含義,他通過monad的類型構(gòu)造器包裝了一個(gè)值。

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

循環(huán)體中每一次迭代生成的結(jié)果都是名稱列表,因此 forM 的結(jié)果是 IO[[FilePath]] ,我們通過 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 中最常見的用途之一。

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

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

為什么提供 mapM 和 forM

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

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

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

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

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

我們可以使用 getRecursiveContents 函數(shù)作為一個(gè)內(nèi)置的簡單文件查找器的基礎(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ù)通過我們?cè)谶^濾器中的謂詞來匹配 getRecursiveContents 函數(shù)返回的名字,每個(gè)通過謂詞判斷的名稱都是文件全路徑,因此如何完成一個(gè)像“查找所有擴(kuò)展名以 .c 結(jié)尾的文件”的功能?

System.FilePath 模塊包含了許多有價(jià)值的函數(shù)來幫助我們操作文件名,在這個(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)行比較的簡單功能的函數(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 在工作中有一些非常刺眼的問題,第一個(gè)就是謂詞并不能準(zhǔn)確而完全的表達(dá),他只關(guān)注文件夾中的實(shí)體名稱,而無法做到辨認(rèn)這是個(gè)文件還是個(gè)目錄此類的事情,——而我們使用 simpleFind 的嘗試就是想列舉所文件和有和文件一樣擁有 .c 擴(kuò)展名的文件夾

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

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

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

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

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

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

讓我們?cè)陬愋拖到y(tǒng)中尋找以寫出擁有更多謂詞,更少bug的代碼,我們通過避免污染IO來堅(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

我們可以通過 doesFileExist 和 doesDirectoryExist 來判斷目錄實(shí)體是目錄還是文件,但暫時(shí)還沒有更多方式來查找這些年里出現(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

如果你無法回憶起 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ù)就是我們手頭所有的一切(我們同樣可以通過黑客手段來獲得文件大小),這些已經(jīng)足夠讓我們明白所感興趣領(lǐng)域中的原則,而非讓我們浪費(fèi)寶貴的時(shí)間對(duì)著一個(gè)例子冥思苦想,如果你需要寫滿足更多需求的代碼, 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è)?自從我們可以通過 Permissions 來判斷實(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é)省一些鍵盤工作和屏幕空間。

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

-- 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)

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

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

這讓我們將目光轉(zhuǎn)移到一個(gè)并不熟悉的函數(shù) filterM 上,它的動(dòng)作就像普通的 filter 函數(shù),但在這種情況下,它在 IOmonad 操作中使用它的謂詞,進(jìn)而通過謂詞表現(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 ,通過 return 語句包裝 p 的IO返回結(jié)果

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

即使 System.Directory 不允許我們獲得一個(gè)文件的大小,我們?nèi)钥梢允褂?System.IO 的類似接口完成這項(xiàng)任務(wù),它包含了一個(gè)名為 hFileSize 的函數(shù),這一函數(shù)返回打開文件的字節(jié)數(shù),下面是他的簡單調(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 包裝起來,他應(yīng)當(dāng)返回一個(gè)空值,而當(dāng)實(shí)體不是文件或者沒有被打開時(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 語句。

我們的異常捕捉在忽略通過的異常的同時(shí)返回一個(gè)空值,函數(shù)體唯一的變化就是允許通過 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í)行,如果無法斷言,則延遲到下一次垃圾回收。

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

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

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

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

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

每當(dāng) openFile 成功之后我們就必須保證調(diào)用 hClose , Control.Exception 模塊提供了 bracket 函數(shù)來支持這個(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ì)通過的每個(gè)請(qǐng)求資源文件的操作,使用和釋放動(dòng)作都是必要的。

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

我們現(xiàn)在可以寫一個(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ù),首先打開文件,并且返回文件句柄,第二步關(guān)閉句柄,第三步在句柄上調(diào)用 hFileSize 并用 just 包裝結(jié)果返回

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

練習(xí)

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

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

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

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

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

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

第一步是寫一個(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è)可以用做縮寫的類型,相似的結(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ì)那些我們無法打開的文件或者不是文件的東西我們返回的實(shí)體大小是 -1 。

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

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

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

equalP 的相等強(qiáng)調(diào)了這一事實(shí),我們認(rèn)為它需要兩個(gè)參數(shù),在 Haskell 柯里化處理了所有函數(shù)的情況下,通過這種方式寫 equalP 并無必要,我們可以避免匿名函數(shù),同時(shí)通過柯里化來寫出表現(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)之前,先把寫好的模塊加載到 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.

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

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

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

多用提升(lifting)少用模板

除了 equalP ,我們還將能夠編寫其他二進(jìn)制函數(shù),我們更希望不去寫他們每個(gè)的具體實(shí)現(xiàn),因?yàn)檫@看起來只是重復(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 ,通過提升(lifting)將它引入到上下文,這解釋了當(dāng)前函數(shù)名稱中l(wèi)ifting出現(xiàn)的原因,提升讓我們復(fù)用代碼并降低模板的使用,在本書的后半部分的內(nèi)容中,我們將大量使用這一技術(shù)

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

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

我們可以通過組合字(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]

謂詞組合

如果我們希望組合謂詞,我們可以循著手邊最明顯的路徑來開始

-- 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)在我們知道了提升,他成為通過提升存在的布爾操作來削減代碼量的更自然的選擇。

-- 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 。

定義并使用新算符

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

-- 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)域語言的操作如果沒有邊界(fixities)聲明將會(huì)被以 infixl9 之類的東西對(duì)待,計(jì)算從左到右,如果跳過這個(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ì)這些就可以寫一個(gè)不用括號(hào)的表達(dá)式,卻和 myTest3 的解析結(jié)果一致的表達(dá)式了

控制遍歷

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

為了應(yīng)付多種情況,我們正在嘗試改變部分表達(dá),為了替代精心刻畫的函數(shù)類型 InfoP ,我們將使用一個(gè)普通代數(shù)數(shù)據(jù)類型來表達(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

記錄語法給我們自由控制函數(shù)的權(quán)限,如 infoPath , traverse 函數(shù)中的這種類型是簡單地,正如我們之前期望的那樣,如果需要一個(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ù)定義,一行行的深入他,解釋它每行為何是這樣,不過開始部分的那幾行沒什么神秘的,它們只是之前看到代碼的拷貝。

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

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

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

本行的新上下文中使用提升技術(shù), liftM 函數(shù)需要一個(gè)規(guī)則函數(shù), concat ,并且提升到 IO 的monad操作,換言之,他需要 forM 通過 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í)如何確定需要通過的內(nèi)容。
  2. 使用 id 作為控制函數(shù), traverseid 扮演一個(gè)前序遞歸樹,在子目錄之前他返回一個(gè)父目錄,寫一個(gè)控制函數(shù)讓 traverse 表現(xiàn)為一個(gè)后序遍歷,返回子目錄在父目錄之前。
  3. 使得《謂詞組合》一節(jié)里面的斷言和組合字可以處理新的 info 類型。
  4. 給 traverse 寫一個(gè)包裝器,讓你通過謂詞控制遞歸,并通過謂詞過濾返回結(jié)果

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

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

-- 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è)新的替代,通過部分應(yīng)用(partial application)和函數(shù)組合(function composition)替代liberally,在 where 塊中我們已經(jīng)定義了一些本地函數(shù),在 maybe 組合子中,使用了 case 表達(dá)式,為了替代 liftM ,我們手動(dòng)將 concat 提升。

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

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

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

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

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

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

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

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

相似的文件夾, foldr 和 foldl' ,干凈的生成遍歷并計(jì)算出一個(gè)結(jié)果,很難把這個(gè)想法從列表擴(kuò)展到目錄樹,但我們?nèi)詷酚谠?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 值來表達(dá)這個(gè)目錄實(shí)體,并返回一個(gè)新種子和一個(gè)我們 fold 函數(shù)的說明,這個(gè)說明通過 Iterate 類型的構(gòu)造器來表達(dá):

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

目錄邏輯上是左序的,因?yàn)槲覀冮_始從我們第一個(gè)遇到的實(shí)體開始 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)

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

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

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

即使 fold 調(diào)用 walk , walk 調(diào)用 fold 這樣的遞歸來遍歷子目錄,每個(gè)函數(shù)返回一個(gè)用 Iterate 包裝起來的種子,當(dāng) fold 被調(diào)用,并且返回, walk 檢查返回并觀察需要繼續(xù)還是退出,通過這種方式,一個(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 來允許調(diào)用改變遍歷目錄實(shí)體的順序。
  2. foldTree 函數(shù)展示了前序遍歷,將它修正為允許調(diào)用方?jīng)Q定便利順序。
  3. 寫一個(gè)組合子的庫允許 foldTree 接收不同類型的 iterators ,你能寫出更簡潔的 iterators 嗎?

代碼指南

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

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

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

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

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

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

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

常用布局風(fēng)格

只要你的代碼遵守布局規(guī)范,那么他并不會(huì)給人一團(tuán)亂麻的感覺,因此也不會(huì)造成誤解,也就是說,有些布局風(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 在一系列等式之后跟著的寫法都是正確的,但下面這種寫法則會(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)即使合法也很少用到,他們的使用并不存在問題,只是讓代碼看起來奇怪,同時(shí)讓Haskell寫成的代碼不必遵守排版規(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)也合法,但不常用,而且容易被誤讀。

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

-- 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 庫并不暴露足夠的消息給我們寫有趣和復(fù)雜的查詢。

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

掃描二維碼

下載編程獅App

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

編程獅公眾號(hào)