第八章:高效文件處理、正則表達(dá)式、文件名匹配

2018-02-24 15:49 更新

第八章:高效文件處理、正則表達(dá)式、文件名匹配

高效文件處理

下面是個(gè)簡(jiǎn)單的基準(zhǔn)測(cè)試,讀取一個(gè)由數(shù)字構(gòu)成的文本文件,并打印它們的和。

-- file: ch08/SumFile.hs
main = do
    contents <- getContents
    print (sumFile contents)
  where sumFile = sum . map read . words

盡管讀寫(xiě)文件時(shí),默認(rèn)使用 String 類型,但它并不高效,所以這樣簡(jiǎn)單的程序效率會(huì)很糟糕。

一個(gè) String 代表一個(gè)元素類型為 Char 的列表;列表的每個(gè)元素被單獨(dú)分配內(nèi)存,并有一定的寫(xiě)入開(kāi)銷。對(duì)那些要讀取文本及二進(jìn)制數(shù)據(jù)的程序來(lái)說(shuō),這些因素會(huì)影響內(nèi)存消耗和執(zhí)行效率。在這個(gè)簡(jiǎn)單的測(cè)試中,即使是 Python 那樣的解釋型語(yǔ)言的表現(xiàn)也會(huì)大大好于使用 String 的 Haskell 代碼。

bytestring 庫(kù)是 String 類型的一個(gè)快速、經(jīng)濟(jì)的替代品。在保持 Haskell 代碼的表現(xiàn)力和簡(jiǎn)潔的同時(shí),使用 bytestring 編寫(xiě)的代碼在內(nèi)存占用和執(zhí)行效率經(jīng)??梢赃_(dá)到或超過(guò) C 代碼。

這個(gè)庫(kù)提供兩個(gè)模塊。每個(gè)都定義了與 String 類型上函數(shù)對(duì)應(yīng)的替代物。

  • Data.ByteString 定義了一個(gè)名為 ByteString 的嚴(yán)格類型,其將一個(gè)字符串或二進(jìn)制數(shù)據(jù)或文本用一個(gè)數(shù)組表示。
  • Data.ByteString.Lazy 模塊定義了一個(gè)惰性類型,同樣命名為 ByteString 。其將字符串?dāng)?shù)據(jù)表示為一個(gè)由 塊 組成的列表,每個(gè)塊是大小為 64KB 的數(shù)組。

這兩種 ByteString 適用于不同的場(chǎng)景。對(duì)于大體積的文件流(幾百 MB 至幾 TB),最好使用惰性的 ByteString 。其塊的大小被調(diào)整得對(duì)現(xiàn)代 CPU 的 L1 緩存特別友好,并且在流中已經(jīng)被處理過(guò)塊可以被垃圾收集器快速丟棄。

對(duì)于不在意內(nèi)存占用而且需要隨機(jī)訪問(wèn)的數(shù)據(jù),最好使用嚴(yán)格的 ByteString 類型。

二進(jìn)制 I/O 和有限載入

讓我們來(lái)開(kāi)發(fā)一個(gè)小函數(shù)以說(shuō)明 ByteString API 的一些用法。我們將檢測(cè)一個(gè)文件是否是 ELF object 文件:這種文件類型幾乎被所有現(xiàn)代類 Unix 系統(tǒng)作為可執(zhí)行文件。

這個(gè)簡(jiǎn)單的問(wèn)題可以通過(guò)查看文件頭部的四個(gè)字節(jié)解決,看他們是否匹配某個(gè)特定的字節(jié)序列。表示某種文件類型的字節(jié)序列通常被稱為 魔法數(shù) 。

-- file: ch08/ElfMagic.hs
import qualified Data.ByteString.Lazy as L

hasElfMagic :: L.ByteString -> Bool
hasElfMagic content = L.take 4 content == elfMagic
    where elfMagic = L.pack [0x7f, 0x45, 0x4c, 0x46]

我們使用 Haskell 的 有限載入 語(yǔ)法載入 ByteString 模塊, 像上面 importqualified 那句那樣。這樣可以把一個(gè)模塊關(guān)聯(lián)到另一個(gè)我們選定的名字。

例如,使用到惰性 ByteString 模塊的 take 函數(shù)時(shí),要寫(xiě)成 L.take ,因?yàn)槲覀儗⑦@個(gè)模塊載入到了 L 這個(gè)名字下。若沒(méi)有明確指明使用哪個(gè)版本的函數(shù),如此處的 take ,編譯器會(huì)報(bào)錯(cuò)。

我們將一直使用有限載入語(yǔ)法使用 ByteString 模塊,因?yàn)槠渲刑峁┑暮芏嗪瘮?shù)與 Prelude 模塊中的函數(shù)重名。

Note

有限載入使得可以方便地切換兩種 ByteString 類型。只需要在代碼的頭部改變 import 聲明;剩余的代碼可能無(wú)需任何修改。你可以方便地比較兩種類型,以觀察哪種類型更符合你程序的需要。

無(wú)論是否使用有限載入,始終可以使用模塊的全名來(lái)識(shí)別某些混淆。例如, Data.ByteString.Lazy.length 和 L.length 表示相同的函數(shù), Prelude.sum 和 sum 也是如此。

ByteString 模塊為二進(jìn)制 I/O 而設(shè)計(jì)。Haskell 中表達(dá)字節(jié)的類型是 Word8 ;如果需要按名字引用它,需要將其從 Data.Word 模塊載入。

L.pack 函數(shù)接受一個(gè)由 Word8 組成的列表,并將其裝入一個(gè)惰性 ByteString ( L.unpack 函數(shù)的作用恰好相反。)。 hasElfMagic 函數(shù)簡(jiǎn)單地將一個(gè) ByteString 的前四字節(jié)與一個(gè)魔法數(shù)相比較。

我們使用了典型的 Haskell 風(fēng)格編寫(xiě) hasElfMagic 函數(shù),其并不執(zhí)行 I/O。這里是如何在真正的文件上使用它。

-- file: ch08/ElfMagic.hs
isElfFile :: FilePath -> IO Bool
isElfFile path = do
  content <- L.readFile path
  return (hasElfMagic content)

L.readFile 函數(shù)是 readFile 的惰性 ByteString 等價(jià)物。它是惰性執(zhí)行的,將文件讀取為數(shù)據(jù)是需要的。它也很高效,立即讀取 64KB 大小的塊。對(duì)我們的任務(wù)而言,惰性 ByteString 是一個(gè)好選擇,我們可以安全的將這個(gè)函數(shù)應(yīng)用在任意大小的文件上。

文本 I/O

方便起見(jiàn), bytestring 庫(kù)提供兩個(gè)具有有限文本 I/O 功能的模塊, Data.ByteString.Char8 和 Data.ByteSring.Lazy.Char8 。它們將每個(gè)字符串的元素暴露為 Char 而非 Word8 。

Warning

這些模塊中的函數(shù)適用于單字節(jié)大小的 Char 值,所以他們僅適用于 ASCII 及某些歐洲字符集。大于 255 的值將被截?cái)唷?/p>

這兩個(gè)面向字符的 bytestring 模塊提供了用于文本處理的函數(shù)。以下文件包含了一家知名互聯(lián)網(wǎng)公司在 2008 年中期每個(gè)月的股價(jià)。

如何在這一系列記錄中找到最高收盤(pán)價(jià)呢?收盤(pán)價(jià)位于以逗號(hào)分隔的第四列。以下函數(shù)從單行數(shù)據(jù)中獲取收盤(pán)價(jià)。

-- file: ch08/HighestClose.hs
import qualified Data.ByteString.Lazy.Char8 as L

closing = readPrice . (!!4) . L.split ','

這個(gè)函數(shù)使用 point-free 風(fēng)格編寫(xiě),我們要從右向左閱讀。 L.split 函數(shù)將一個(gè)惰性 ByteString 按某個(gè)分隔符切分為一個(gè)由 ByteString 組成的列表。 (!!) 操作符檢索列表中的第 k 個(gè)元素。 readPrice 函數(shù)將一個(gè)表示小數(shù)的字符串轉(zhuǎn)換為一個(gè)數(shù)。

- file: ch08/HighestClose.hs
readPrice :: L.ByteString -> Maybe Int
readPrice str =
    case L.readInt str of
      Nothing             -> Nothing
      Just (dollars,rest) ->
        case L.readInt (L.tail rest) of
          Nothing           -> Nothing
          Just (cents,more) ->
            Just (dollars * 100 + cents)

我們使用 L.readInt 函數(shù)來(lái)解析一個(gè)整數(shù)。當(dāng)發(fā)現(xiàn)數(shù)字時(shí),它會(huì)將一個(gè)整數(shù)和字符串的剩余部分一起返回。 L.readInt 在解析失敗時(shí)返回 Nothing ,這導(dǎo)致我們的函數(shù)稍有些復(fù)雜。

查找最高收盤(pán)價(jià)的函數(shù)很容易編寫(xiě)。

-- file: ch08/HighestClose.hs
highestClose = maximum . (Nothing:) . map closing . L.lines

highestCloseFrom path = do
    contents <- L.readFile path
    print (highestClose contents)

不能對(duì)空列表使用 maximum 函數(shù),所以我們耍了點(diǎn)小把戲。

ghci> maximum [3,6,2,9]
9
ghci> maximum []
*** Exception: Prelude.maximum: empty list

我們想在沒(méi)有股票數(shù)據(jù)時(shí)也不拋出異常,所以用 (Nothing:) 這個(gè)表達(dá)式來(lái)確保輸入到 maximum 函數(shù)的由 MaybeInt 值構(gòu)成的列表總是非空。

ghci> maximum [Nothing, Just 1]
Just 1
ghci> maximum [Nothing]
Nothing

我們的函數(shù)工作正常嗎?

ghci> :load HighestClose
[1 of 1] Compiling Main             ( HighestClose.hs, interpreted )
Ok, modules loaded: Main.
ghci> highestCloseFrom "prices.csv"
Loading package array-0.1.0.0 ... linking ... done.
Loading package bytestring-0.9.0.1 ... linking ... done.
Just 2741

因?yàn)槲覀儼堰壿嫼?I/O 分離開(kāi)了,所以即使不創(chuàng)建一個(gè)空文件也可以測(cè)試無(wú)數(shù)據(jù)的情況。

ghci> highestClose L.empty
Nothing

匹配文件名

很多面向操作系統(tǒng)的編程語(yǔ)言提供了檢測(cè)某個(gè)文件名是否匹配給定模式的庫(kù)函數(shù),或者返回一個(gè)匹配給定模式的文件列表。在其他語(yǔ)言中,這個(gè)函數(shù)通常叫做 fmatch 。盡管 Haskell 標(biāo)準(zhǔn)庫(kù)提供了很多有用的系統(tǒng)編程設(shè)施,但是并沒(méi)有提供這類用于匹配文件名的函數(shù)。所以我們可以自己開(kāi)發(fā)一個(gè)。

我們需要處理的模式種類通常稱為 glob 模式(我們將使用這個(gè)術(shù)語(yǔ)),通配符模式,或稱 shell 風(fēng)格模式。它們僅是一些簡(jiǎn)單規(guī)則。你可能已經(jīng)了解了,但是這里將做一個(gè)簡(jiǎn)要的回顧。

Note

  • 對(duì)某個(gè)模式的匹配從字符串頭部開(kāi)始,在字符串尾部結(jié)束。
  • 多數(shù)文本字符匹配自身。例如,文本 foo 作為模式匹配其自身 foo ,且在一個(gè)輸入字符串中僅匹配 foo 。
    • (星號(hào)) 意味著 “匹配所有”; 其將匹配所有文本,包括空字符串。 例如, 模式 foo 將匹配任意以 foo 開(kāi)頭的字符串,比如 foo 自身, foobar , 或 foo.c 。 模式 quux.c 將匹配任何以 quux 開(kāi)頭且以 .c 結(jié)束的字符串,如 quuxbaz.c 。
  • ? (問(wèn)號(hào)) 匹配任意單個(gè)字符。模式 pic??.jpg 將匹配類似 picaa.jpg 或 pic01.jpg 的文件名。
  • [ (左方括號(hào)) 將開(kāi)始定義一個(gè)字符類,以 ] 結(jié)束。其意思是 “匹配在這個(gè)字符類中的任意字符”。 [! 開(kāi)啟一個(gè)否定的字符類,其意為 “匹配不在這個(gè)字符類中的任意字符”。

用 - (破折號(hào)) 連接的兩個(gè)字符,是一種表示范圍的速記方法,表示:“匹配這個(gè)圍內(nèi)的任意字符”。

字符類有一個(gè)附加的條件;其不可為空。在 [ 或 [! 后的字符是這個(gè)字符類的一部分,所以我們可以編寫(xiě)包含 ] 的字符類,如 []aeiou] 。模式 pic[0-9].[pP][nN][gG] 將匹配由字符串 pic 開(kāi)始,跟隨單個(gè)數(shù)字,最后是字符串 .png 的任意大小寫(xiě)形式。

盡管 Haskell 的標(biāo)準(zhǔn)庫(kù)沒(méi)有提供匹配 glob 模式的方法,但它提供了一個(gè)良好的正則表達(dá)式庫(kù)。Glob 模式僅是一個(gè)從正則表達(dá)式中切分出來(lái)的略有不同的子集。很容易將 glob 模式轉(zhuǎn)換為正則表達(dá)式,但在此之前,我們首先要了解怎樣在 Haskell 中使用正則表達(dá)式。

Haskell 中的正則表達(dá)式

在這一節(jié),我們將假設(shè)讀者已經(jīng)熟悉 Python、Perl 或 Java 等其他語(yǔ)言中的正則表達(dá)式。

為了簡(jiǎn)潔,此后我們將 “regular expression” 簡(jiǎn)寫(xiě)為 regexp。

我們將以與其他語(yǔ)言對(duì)比的方式介紹 Haskell 如何處理 regexp,而非從頭講解何為 regexp。Haskell 的正則表達(dá)式庫(kù)比其他語(yǔ)言具備更加強(qiáng)大的表現(xiàn)力,所以我們有很多可以聊的。

在我們對(duì) regexp 庫(kù)的探索開(kāi)始時(shí),只需使用 Text.Regex.Posix 工作。一般通過(guò)在 ghci 進(jìn)行交互是探索一個(gè)模塊最方便的辦法。

ghci> :module +Text.Regex.Posix

可能正則表達(dá)式匹配函數(shù)是我們平時(shí)需要使用的唯一的函數(shù),其以中綴預(yù)算符 (=~) (從 Perl 中借鑒) 表示。要克服的第一個(gè)障礙是 Haskell 的 regexp 庫(kù)重度使用了多態(tài)。其結(jié)果就是, (=~) 的類型簽名非常難懂,所以我們?cè)诖藢?duì)其不做解釋。 =~ 操作符的參數(shù)和返回值都使用了類型類。第一個(gè)參數(shù) (=~ 左側(cè)) 是要被匹配的文本;第二個(gè)參數(shù) (=~ 右側(cè)) 是準(zhǔn)備匹配的正則表達(dá)式。對(duì)每個(gè)參數(shù)我們都可以使用 String 或者 ByteString 。 結(jié)果的多種類型 =~ 操作符的返回類型是多態(tài)的,所以 Haskell 編譯器需要一通過(guò)一些途徑知道我們想獲得哪種類型的結(jié)果。實(shí)際編碼中,可以通過(guò)我們?nèi)绾问褂闷ヅ浣Y(jié)果推導(dǎo)出它的類型。但是當(dāng)我們通過(guò) ghci 進(jìn)行探索時(shí),缺少類型推導(dǎo)的線索。如果不指明匹配結(jié)果的類型,ghci 將因其無(wú)法獲得足夠信息對(duì)匹配結(jié)果進(jìn)行類型推導(dǎo)而報(bào)錯(cuò)。 當(dāng) ghci 無(wú)法推斷目標(biāo)的類型時(shí),我們要告訴它想要哪種類型。若想知道正則匹配是否通過(guò)時(shí),需要將結(jié)果類型指定為 Bool 型。

ghci> "my left foot" =~ "foo" :: Bool
Loading package array-0.1.0.0 ... linking ... done.
Loading package containers-0.1.0.1 ... linking ... done.
Loading package bytestring-0.9.0.1 ... linking ... done.
Loading package mtl-1.1.0.0 ... linking ... done.
Loading package regex-base-0.93.1 ... linking ... done.
Loading package regex-posix-0.93.1 ... linking ... done.
True
ghci> "your right hand" =~ "bar" :: Bool
False
ghci> "your right hand" =~ "(hand|foot)" :: Bool
True

在 regexp 庫(kù)內(nèi)部,有一種類型類名為 RegexContext ,其描述了目標(biāo)類型的行為?;A(chǔ)庫(kù)定義了很多這個(gè)類型類的實(shí)例。 Bool 型是這種類型類的一個(gè)實(shí)例,所以我們?nèi)』亓艘粋€(gè)可用的結(jié)果. 另一個(gè)實(shí)例是 Int ,可以描述正則表達(dá)式匹配了多少次。

ghci> "a star called henry" =~ "planet" :: Int
0
ghci> "honorificabilitudinitatibus" =~ "[aeiou]" :: Int
13

如果指定結(jié)果類型為 String ,將得到第一個(gè)匹配的子串,或者表示無(wú)匹配的空字符串。

ghci> "I, B. Ionsonii, uurit a lift'd batch" =~ "(uu|ii)" :: String
"ii"
ghci> "hi ludi, F. Baconis nati, tuiti orbi" =~ "Shakespeare" :: String
""

另一個(gè)合法的返回值類型是 [[Srtring]] ,將返回由所有匹配的的字符串組成的列表。

ghci> "I, B. Ionsonii, uurit a lift'd batch" =~ "(uu|ii)" :: [[String]]
[["ii","ii"],["uu","uu"]]
ghci> "hi ludi, F. Baconis nati, tuiti orbi" =~ "Shakespeare" :: [[String]]
[]

Warning

注意 String 類型的結(jié)果

指定結(jié)果為普通的字符串時(shí),要當(dāng)心。因?yàn)?(=~) 在表示 “無(wú)匹配” 時(shí)會(huì)返回空字符串,很明顯這導(dǎo)致了難以處理可以匹配空字符串的正則表達(dá)式。這情況出現(xiàn)時(shí),就需要使用另一種不同的結(jié)果類型,比如 [[String]]。

以上是一些 “簡(jiǎn)單”的結(jié)果類型,不過(guò)還沒(méi)說(shuō)完。在繼續(xù)講解之前,我們先來(lái)定義一個(gè)在之后的例子中共同使用的模式串。可以在 ghci 中將這個(gè)模式串定義為一個(gè)變量,以便節(jié)省一些輸入操作。

ghci> let pat = "(foo[a-z]*bar|quux)"

當(dāng)模式匹配了字符串時(shí),可以獲取很多關(guān)于上下文的信息。如果指定 (String,String,String) 類型的元組作為結(jié)果類型,可以獲取字符串中首次匹配之前的部分,首次匹配的子串,和首次匹配之后的部分。

ghci> "before foodiebar after" =~ pat :: (String,String,String)
("before ","foodiebar"," after")

若匹配失敗,整個(gè)字符串會(huì)作為 “首次匹配之前” 的部分返回,元組的其他兩個(gè)元素將為空字符串。

ghci> "no match here" =~ pat :: (String,String,String)
("no match here","","")

使用四元組作為返回結(jié)果時(shí),元組的第四個(gè)元素是一個(gè)包含了模式中所有分組的列表。

ghci> "before foodiebar after" =~ pat :: (String,String,String,[String])
("before ","foodiebar"," after",["foodiebar"])

也可以獲得關(guān)于匹配結(jié)果的數(shù)字信息。二元組類型的結(jié)果可以表示首次匹配在字符串中的偏移,以及匹配結(jié)果的長(zhǎng)度。如果使用由這種二元組構(gòu)成的列表作為結(jié)果類型,我們將得到所有字符串中所有匹配的此類信息。

ghci> "before foodiebar after" =~ pat :: (Int,Int)
(7,9)
ghci> getAllMatches  ("i foobarbar a quux" =~ pat) :: [(Int,Int)]
[(2,9),(14,4)]

二元組的首個(gè)元素(表示偏移的那個(gè)),其值為 -1 時(shí),表示匹配失敗。當(dāng)指定返回值為列表時(shí),空表表示失敗。

ghci> "eleemosynary" =~ pat :: (Int,Int)
(-1,0)
ghci> "mondegreen" =~ pat :: [(Int,Int)]
[]

以上并非 RegexContext 類型類的內(nèi)置實(shí)例的完整清單。完整的清單可以在 Text.Regex.Base.Context 模塊的文檔中找到。

使函數(shù)具有多態(tài)返回值的能力對(duì)于一個(gè)靜態(tài)類型語(yǔ)言來(lái)說(shuō)是個(gè)不同尋常的特性。

進(jìn)一步了解正則表達(dá)式

不同類型字符串的混合與匹配

之前提到過(guò), =~ 操作符的輸入和返回值都使用了類型類。我們可以在正則表達(dá)式和要匹配的文本中使用 String 或者嚴(yán)格的 ByteString 類型。

ghci> :module +Data.ByteString.Char8
ghci> :type pack "foo"
pack "foo" :: ByteString

我們可以嘗試不同的 String 和 ByteString 組合。

ghci> pack "foo" =~ "bar" :: Bool
False
ghci> "foo" =~ pack "bar" :: Int
0
ghci> getAllMatches (pack "foo" =~ pack "o") :: [(Int, Int)]
[(1,1),(2,1)]

不過(guò),我們需要注意,文本匹配的結(jié)果必須于被匹配的字符串類型一致。讓我們實(shí)踐一下,看這是什么意思。

ghci> pack "good food" =~ ".ood" :: [[ByteString]]
[["good"],["food"]]

上面的例子中,我們使用 pack 將一個(gè) String 轉(zhuǎn)換為 ByteString 。這種情況可以通過(guò)類型檢查,因?yàn)?ByteString 也是一種合法的結(jié)果類型。但是如果輸入字符串類型為 String 類型,在嘗試獲得 ByteString 類型結(jié)果時(shí)將會(huì)失敗。

ghci> "good food" =~ ".ood" :: [[ByteString]]

<interactive>:55:13:
    No instance for (RegexContext Regex [Char] [[ByteString]])
      arising from a use of ‘=~’
    In the expression: "good food" =~ ".ood" :: [[ByteString]]
    In an equation for ‘it’:
        it = "good food" =~ ".ood" :: [[ByteString]]

將結(jié)果類型指定為與被匹配字符串相同的 String 類型就可以輕松地解決這個(gè)問(wèn)題。

ghci> "good food" =~ ".ood" :: [[String]]
[["good"],["food"]]

對(duì)于正則表達(dá)式不存在這個(gè)限制。正則表達(dá)式可以是 String 或 ByteString ,而不必在意輸入或結(jié)果是何種類型。

你要知道的其他一些事情

查閱 Haskell 的庫(kù)文檔,會(huì)發(fā)現(xiàn)很多和正則表達(dá)式有關(guān)的模塊。 Text.Regex.Base 下的模塊定義了供其他所有正則表達(dá)式庫(kù)使用的通用 API 。可以同時(shí)安裝許多不同實(shí)現(xiàn)的正則表達(dá)式模塊。寫(xiě)作本書(shū)時(shí), GHC 自帶一個(gè)實(shí)現(xiàn), Text.Regex.Posix 。正如其名字,這個(gè)模塊提供了 POSIX 語(yǔ)義的正則表達(dá)式實(shí)現(xiàn)。

Note

Perl 風(fēng)格和 POSIX 風(fēng)格的正則表達(dá)式

如果你此前用過(guò)其他語(yǔ)言,如 Perl,Python,或 Java,并且使用過(guò)其中的正則表達(dá)式, 你應(yīng)該知道 Text.Regex.Posix 模塊處理的 POSIX 風(fēng)格的正則表達(dá)式與 Perl 風(fēng)格的正則表達(dá)式有一些顯著的不同。

當(dāng)有多個(gè)匹配結(jié)果候選時(shí),Perl 的正則表達(dá)式引擎表現(xiàn)為左側(cè)最小匹配,而 POSIX 引擎會(huì)選擇貪婪匹配(最長(zhǎng)匹配)。當(dāng)使用正則表達(dá)式 (foo|fo*) 匹配字符串 foooooo 時(shí),Perl 風(fēng)格引擎將返回 foo (最左的匹配),而 POSIX 引擎將返回的結(jié)果將包含整個(gè)字符串 (貪婪匹配)。

POSIX 正則表達(dá)式比 Perl 風(fēng)格的正則表達(dá)式缺少一些格式語(yǔ)法。它們也缺少一些 Perl 風(fēng)格正則表達(dá)式的功能,比如零寬度斷言和對(duì)貪婪匹配的控制。

Hackage 上也有其他 Haskell 正則表達(dá)式包可供下載。其中一些比內(nèi)置的 POSIX 引擎擁有更好的執(zhí)行效率 (如 regex-tdfa); 另外一些提供了大多數(shù)程序員熟悉的 Perl 風(fēng)格正則匹配 (如 regex-pcre)。它們都按照我們這節(jié)提到的 API 編寫(xiě)。

將 glob 模式翻譯為正則表達(dá)式

我們已經(jīng)看到了用正則表達(dá)式匹配文本的多種方法,現(xiàn)在讓我們將注意力回到 glob 模式。我們要編寫(xiě)一個(gè)函數(shù),接收一個(gè) glob 模式作為輸入,返回其對(duì)應(yīng)的正則表達(dá)式。glob 模式和正則表達(dá)式都以文本字符串表示,所以這個(gè)函數(shù)的類型應(yīng)該已經(jīng)清楚了。

-- file: ch08/GlobRegex.hs
module GlobRegex
    (
      globToRegex
    , matchesGlob
    ) where

import Text.Regex.Posix ((=~))

globToRegex :: String -> String

我們生成的正則表達(dá)式必須被錨定,所以它要對(duì)一個(gè)字符串從頭到尾完整匹配。

-- file: ch08/GlobRegex.hs
globToRegex cs = '^' : globToRegex' cs ++ "$"

回想一下, String 僅是 [Char] 的同義詞,一個(gè)由字符組成的數(shù)組。 : 操作符將一個(gè)值加入某個(gè)列表頭部,此處是將字符 ^ 加入 globToRegex' 函數(shù)返回的列表頭部。

Note

在定義之前使用一個(gè)值

Haskell 在使用某個(gè)值或函數(shù)時(shí),并不需要其在之前的源碼中被聲明。在某個(gè)值首次被使用之后才定義它是很平常的。Haskell 編譯器并不關(guān)心這個(gè)層面上的順序。這使我們可以用最符合邏輯的方式靈活地組織代碼,而不是為使編譯器作者更輕松而遵守某種順序。

Haskell 模塊的作者們經(jīng)常利用這種靈活性,將“更重要的”代碼放在源碼文件更靠前的位置,將繁瑣的實(shí)現(xiàn)放在后面。這也是我們實(shí)現(xiàn) globToRegex' 函數(shù)及其輔助函數(shù)的方法。

globToRegex' 將使用正則表達(dá)式做大部分的翻譯工作。我們將使用 Haskell 的模式匹配特性輕松地窮舉出需要處理的每一種情況

-- file: ch08/GlobRegex.hs

globToRegex' :: String -> String
globToRegex' "" = ""

globToRegex' ('*':cs) = ".*" ++ globToRegex' cs

globToRegex' ('?':cs) = '.' : globToRegex' cs

globToRegex' ('[':'!':c:cs) = "[^" ++ c : charClass cs
globToRegex' ('[':c:cs)     = '['  :  c : charClass cs
globToRegex' ('[':_)        = error "unterminated character class"

globToRegex' (c:cs) = escape c ++ globToRegex' cs

我們的第一條規(guī)則是,如果觸及 glob 模式的尾部(也就是說(shuō)當(dāng)輸入為空字符串時(shí)),我們返回 $ ,正則表達(dá)式中表示“匹配行尾”的符號(hào)。我們按照這樣一系列規(guī)則將模式串由 glob 語(yǔ)法轉(zhuǎn)化為正則表達(dá)式語(yǔ)法。最后一條規(guī)則匹配所有字符,首先將可轉(zhuǎn)義字符進(jìn)行轉(zhuǎn)義。

escape 函數(shù)確保正則表達(dá)式引擎不會(huì)將普通字符串解釋為構(gòu)成正則表達(dá)式語(yǔ)法的字符。

-- file: ch08/GlobRegex.hs
escape :: Char -> String
escape c | c `elem` regexChars = '\\' : [c]
         | otherwise = [c]
    where regexChars = "\\+()^$.{}]|"

charClass 輔助函數(shù)僅檢查一個(gè)字符類是否正確地結(jié)束。這個(gè)并不改變其輸入,直到遇到一個(gè) ] 字符,其將控制流交還給 globToRegex'

-- file: ch08/GlobRegex.hs
charClass :: String -> String
charClass (']':cs) = ']' : globToRegex' cs
charClass (c:cs)   = c : charClass cs
charClass []       = error "unterminated character class"

現(xiàn)在我們已經(jīng)完成了 globToRegex 函數(shù)及其輔助函數(shù)的定義,讓我們?cè)?ghci 中裝載并且實(shí)驗(yàn)一下。

ghci> :load GlobRegex.hs
[1 of 1] Compiling GlobRegex        ( GlobRegex.hs, interpreted )
Ok, modules loaded: GlobRegex.
ghci> :module +Text.Regex.Posix
ghci> globToRegex "f??.c"
Loading package array-0.1.0.0 ... linking ... done.
Loading package containers-0.1.0.1 ... linking ... done.
Loading package bytestring-0.9.0.1 ... linking ... done.
Loading package mtl-1.1.0.0 ... linking ... done.
Loading package regex-base-0.93.1 ... linking ... done.
Loading package regex-posix-0.93.1 ... linking ... done.
"^f..\\.c$"

果然,看上去像是一個(gè)合理的正則表達(dá)式??梢允褂盟齺?lái)匹配某個(gè)字符串碼?

ghci> "foo.c" =~ globToRegex "f??.c" :: Bool
True
ghci> "test.c" =~ globToRegex "t[ea]s*" :: Bool
True
ghci> "taste.txt" =~ globToRegex "t[ea]s*" :: Bool
True

奏效了!現(xiàn)在讓我們?cè)?ghci 里玩耍一下。我們可以臨時(shí)定義一個(gè) fnmatch 函數(shù),并且試用它。

ghci> let fnmatch pat name  =  name =~ globToRegex pat :: Bool
ghci> :type fnmatch
fnmatch :: (RegexLike Regex source1) => String -> source1 -> Bool
ghci> fnmatch "d*" "myname"
False

但是 fnmatch 沒(méi)有真正的 “Haskell 味道”。目前為止,最常見(jiàn)的 Haskell 風(fēng)格是賦予函數(shù)具有描述性的,“駝峰式” 命名。將單詞連接為駝峰狀,首字母小寫(xiě)后面每個(gè)單詞的首字母大寫(xiě)。例如,“file name matches” 這幾個(gè)詞將轉(zhuǎn)換為 fileNameMatch 這個(gè)名字。 “駝峰式” 這種說(shuō)法來(lái)自與大寫(xiě)字母形成的“駝峰”。在我們的庫(kù)中,將使用 matchesGlob 這個(gè)函數(shù)名。

-- file: ch08/GlobRegex.hs
matchesGlob :: FilePath -> String -> Bool
name `matchesGlob` pat = name =~ globToRegex pat

你可能注意到目前為止我們使用的都是短變量名。從經(jīng)驗(yàn)來(lái)看,描述性的名字在更長(zhǎng)的函數(shù)定義中更有用,它們有助于可讀性。對(duì)一個(gè)僅有兩行的函數(shù)來(lái)說(shuō),長(zhǎng)變量名價(jià)值較小。

練習(xí)

  1. 使用 ghci 探索當(dāng)你向 globToRegex 傳入一個(gè)畸形的模式時(shí)會(huì)發(fā)生什么,如 “[” 。編寫(xiě)一個(gè)小函數(shù)調(diào)用 globToRegex ,向其傳入一個(gè)畸形的模式。發(fā)生了什么?
  2. Unix 的文件系統(tǒng)的文件名通常是對(duì)大小寫(xiě)敏感的(如:”G” 和 “g” 不同),Windows 文件系統(tǒng)則不是。為 globToRegex 和 matchesGlob 函數(shù)添加一個(gè)參數(shù),以控制它們是否大小寫(xiě)敏感。

重要的題外話:編寫(xiě)惰性函數(shù)

在命令式語(yǔ)言中, globToRegex 通常是個(gè)被我們寫(xiě)成循環(huán)的函數(shù)。舉個(gè)例子,Python 標(biāo)準(zhǔn)庫(kù)中的 fnmatch 模塊包括了一個(gè)名叫 translate 的函數(shù)與我們的 globToRegex 函數(shù)做了完全相同的工作。它就被寫(xiě)成一個(gè)循環(huán)。

如果你了解過(guò)函數(shù)式編程語(yǔ)言比如 Scheme 或 ML ,可能有個(gè)概念已經(jīng)深入你的腦海,“模擬一個(gè)循環(huán)的方法是使用尾遞歸”。

觀察 globToRegex' ,可以發(fā)現(xiàn)其不是一個(gè)尾遞歸函數(shù)。至于原因,重新檢查一下它的最后一組規(guī)則(它的其他規(guī)則也類似)。

-- file: ch08/GlobRegex.hs
globToRegex' (c:cs) = escape c ++ globToRegex' cs

其遞歸地執(zhí)行自身,并以遞歸執(zhí)行的結(jié)果作為 (++) 函數(shù)的參數(shù)。因?yàn)檫f歸執(zhí)行并不是這個(gè)函數(shù)的最后一個(gè)操作,所以 globToRegex' 不是尾遞歸函數(shù)。

為何我們的函數(shù)沒(méi)有定義成尾遞歸的?答案是 Haskell 的非嚴(yán)格求值策略。在我們開(kāi)始討論它之前,先快速的了解一下為什么,傳統(tǒng)編程語(yǔ)言中,這類遞歸定義是我們要避免的。這里有一個(gè)簡(jiǎn)化的 (++) 操作符定義。它是遞歸的,但不是尾遞歸的。

-- file: ch08/append.hs
(++) :: [a] -> [a] -> [a]

(x:xs) ++ ys = x : (xs ++ ys)
[]     ++ ys = ys

在嚴(yán)格求值語(yǔ)言中,如果我們執(zhí)行 “foo” ++ “bar”,將馬上構(gòu)建并返回整個(gè)列表。非嚴(yán)格求值將這項(xiàng)工作延后很久執(zhí)行,知道其結(jié)果在某處被用到。

如果我們需要 “foo” ++ “bar” 這個(gè)表達(dá)式結(jié)果中的一個(gè)元素,函數(shù)定義中的第一個(gè)模式被匹配,返回表達(dá)式 x : (xs ++ ys)。因?yàn)?(:) 構(gòu)造器是非嚴(yán)格的,xs ++ ys 的求值被延遲到當(dāng)我們需要生成更多結(jié)果中的元素時(shí)。當(dāng)生成了結(jié)果中的更多元素,我們不再需要 x ,垃圾收集器可以將其回收。因?yàn)槲覀儼葱枰?jì)算結(jié)果中的元素,且不保留已經(jīng)計(jì)算出的結(jié)果,編譯器可以用常數(shù)空間對(duì)我們的代碼求值。

利用我們的模式匹配器

有一個(gè)函數(shù)可以匹配 glob 模式很好,但我們希望可以在實(shí)際中使用它。在類 Unix 系統(tǒng)中,glob 函數(shù)返回一個(gè)由匹配給定 glob 模式串的文件和目錄組成的列表。讓我們用 Haskell 構(gòu)造一個(gè)類似的函數(shù)。按 Haskell 的描述性命名規(guī)范,我們將這個(gè)函數(shù)稱為 namesMatching 。

-- file: ch08/Glob.hs
module Glob (namesMatching) where

我們將 namesMatching 指定為我們的 Glob 模塊中唯一對(duì)用戶可見(jiàn)的名字。

-- file: ch08/Glob.hs
import System.Directory (doesDirectoryExist, doesFileExist,
                      getCurrentDirectory, getDirectoryContents)

System.FilePath 抽象了操作系統(tǒng)路徑名稱的慣例。(>) 函數(shù)將兩個(gè)部分組合為一個(gè)路徑。

ghci> :m +System.FilePath
ghci> "foo" </> "bar"
Loading package filepath-1.1.0.0 ... linking ... done.
"foo/bar"

The name of the dropTrailingPathSeparator function is perfectly descriptive. No commentsdropTrailingPathSeparator 函數(shù)的名字完美地描述了其作用。

ghci> dropTrailingPathSeparator "foo/"
"foo"

splitFileName 函數(shù)以路徑中的最后一個(gè)斜線將路徑分割為兩部分。

ghci> splitFileName "foo/bar/Quux.hs"
("foo/bar/","Quux.hs")
ghci> splitFileName "zippity"
("","zippity")

配合 Systems.FilePath 和 Systems.Directory 兩個(gè)模塊,我們可以編寫(xiě)一個(gè)在類 Unix 和 Windows 系統(tǒng)上都可以運(yùn)行的可移植的 namesMatching 函數(shù)。

-- file: ch08/Glob.hs
import System.FilePath (dropTrailingPathSeparator, splitFileName, (</>))

在這個(gè)模塊中,我們將模擬一個(gè) “for” 循環(huán);首次嘗試在 Haskell 中處理異常;當(dāng)然還會(huì)用到我們剛寫(xiě)的 matchesGlob 函數(shù)。

-- file: ch08/Glob.hs
import Control.Exception (handle, SomeException)
import Control.Monad (forM)
import GlobRegex (matchesGlob)

目錄和文件存在于各種帶有副作用的活動(dòng)的“真實(shí)世界”,我們的 glob 模式處理函數(shù)的返回值類型中將必須帶有 IO 。

如果的輸入字符串中不包含模式字符,我們簡(jiǎn)單的在文件系統(tǒng)中檢查輸入的名字是否已經(jīng)建立。(注意,此處使用 Haskell 的 guard 語(yǔ)法可以編寫(xiě)精細(xì)整齊的定義?!癷f” 語(yǔ)句也可以做到,但是在美學(xué)上不能令人滿意。 )

-- file: ch08/Glob.hs
isPattern :: String -> Bool
isPattern = any (`elem` "[?*")

namesMatching pat
  | not (isPattern pat) = do
    exists <- doesNameExist pat
    return (if exists then [pat] else [])

doesNameExist 是一個(gè)我們將要簡(jiǎn)要定義的函數(shù)的名字。

如果字符串是一個(gè) glob 模式呢?繼續(xù)定義我們的函數(shù)。

-- file: ch08/Glob.hs
  | otherwise = do
    case splitFileName pat of
      ("", baseName) -> do
          curDir <- getCurrentDirectory
          listMatches curDir baseName
      (dirName, baseName) -> do
          dirs <- if isPattern dirName
                  then namesMatching (dropTrailingPathSeparator dirName)
                  else return [dirName]
          let listDir = if isPattern baseName
                        then listMatches
                        else listPlain
          pathNames <- forM dirs $ \dir -> do
                           baseNames <- listDir dir baseName
                           return (map (dir </>) baseNames)
          return (concat pathNames)

我們使用 splitFileName 將字符串分割為目錄名和文件名。如果第一個(gè)元素為空,說(shuō)明我們正在當(dāng)前目錄尋找符合模式的文件。否則,我們必須檢查目錄名,觀察其是否包含模式。若不含模式,我們建立一個(gè)只由目錄名一個(gè)元素組成的列表。如果含有模式,我們列出所有匹配的目錄。

Note

注意事項(xiàng)

System.FilePath 模塊稍有點(diǎn)詭異。上面的情況就是一個(gè)例子。 splitFileName 函數(shù)在其返回值的目錄名部分的結(jié)尾保留了一個(gè)斜線。

ghci> :module +System.FilePath
ghci> splitFileName "foo/bar"
Loading package filepath-1.1.0.0 ... linking ... done.
("foo/","bar")

If we didn't remember (or know enough) to remove that slash, we'd recurse endlessly in namesMatching, because of the following behaviour of splitFileName. 1 comment 如果忘記(或不夠了解)要去掉這個(gè)斜線,我們將在 namesMatching 函數(shù)中進(jìn)行無(wú)止盡的遞歸匹配,看看后面演示的 splitFileName 的行為你就會(huì)明白。

ghci> splitFileName "foo/"
("foo/","")

你或許能夠想想想象是什么促使我們加入這份注意事項(xiàng)。

最終,我們將每個(gè)目錄中的匹配收集起來(lái),得到一個(gè)由列表組成的列表,然后將它們連接為一個(gè)單獨(dú)的由文件名組成的列表。

上面那個(gè)函數(shù)中出現(xiàn)的陌生的 forM 函數(shù),其行為有些像 “for” 循環(huán):它將其第二個(gè)參數(shù)(一個(gè)動(dòng)作)映射到其第一個(gè)參數(shù)(一個(gè)列表),并返回由其結(jié)果組成的列表。

我們還剩余一些零散的目標(biāo)需要完成。首先是上面用到過(guò)的 doesNameExist 函數(shù)的定義。 System.Directory 函數(shù)無(wú)法檢查一個(gè)名字是否已經(jīng)在文件系統(tǒng)中建立。它強(qiáng)制我們明確要檢查的是一個(gè)文件還是目錄。這個(gè) API 設(shè)計(jì)的很丑陋,所以我們必須在一個(gè)函數(shù)中完成兩次檢驗(yàn)。出于效率考慮,我們首先檢查文件名,因?yàn)槲募饶夸浉R?jiàn)。

-- file: ch08/Glob.hs
doesNameExist :: FilePath -> IO Bool

doesNameExist name = do
    fileExists <- doesFileExist name
    if fileExists
      then return True
      else doesDirectoryExist name

還有兩個(gè)函數(shù)需要定義,返回值都是由某個(gè)目錄下的名字組成的列表。 listMatches 函數(shù)返回由某目錄下全部匹配給定 glob 模式的文件名組成的列表。

-- file: ch08/Glob.hs
listMatches :: FilePath -> String -> IO [String]
listMatches dirName pat = do
    dirName' <- if null dirName
                then getCurrentDirectory
                else return dirName
    handle (const (return [])::(SomeException->IO [String]))
           $ do names <- getDirectoryContents dirName'
                let names' = if isHidden pat
                             then filter isHidden names
                             else filter (not . isHidden) names
                return (filter (`matchesGlob` pat) names')

isHidden ('.':_) = True
isHidden _       = False

listPlain 接收的函數(shù)名若存在,則返回由這個(gè)文件名組成的單元素列表,否則返回空列表。

-- file: ch08/Glob.hs
listPlain :: FilePath -> String -> IO [String]
listPlain dirName baseName = do
    exists <- if null baseName
              then doesDirectoryExist dirName
              else doesNameExist (dirName </> baseName)
    return (if exists then [baseName] else [])

仔細(xì)觀察 listMatches 函數(shù)的定義,將發(fā)現(xiàn)一個(gè)名為 handle 的函數(shù)。之前,我們從 Control.Exception 模塊中將其載入。正如其暗示的那樣,這個(gè)函數(shù)讓我們初次體驗(yàn)了 Haskell 中的異常處理。把它扔進(jìn) ghci 中看我們會(huì)發(fā)現(xiàn)什么。

ghci> :module +Control.Exception
ghci> :type handle
handle :: (Exception -> IO a) -> IO a -> IO a

可以看出 handle 接受兩個(gè)參數(shù)。首先是一個(gè)函數(shù),其接受一個(gè)異常值,且有副作用(其返回值類型帶有 IO 標(biāo)簽);這是一個(gè)異常處理器。第二個(gè)參數(shù)是可能會(huì)拋出異常的代碼。 關(guān)于異常處理器,異常處理器的類型限制其必須返回與拋出異常的代碼相同的類型。所以它只能選擇或是拋出一個(gè)異常,或像在我們的例子中返回一個(gè)由字符串組成的列表。 const 函數(shù)接受兩個(gè)參數(shù);無(wú)論第二個(gè)參數(shù)是什么,其始終返回第一個(gè)參數(shù)。

ghci> :type const
const :: a -> b -> a
ghci> :type return []
return [] :: (Monad m) => m [a]
ghci> :type handle (const (return []))
handle (const (return [])) :: IO [a] -> IO [a]

我們使用 const 編寫(xiě)異常處理器忽略任何向其傳入的異常。取而代之,當(dāng)我們捕獲異常時(shí),返回一個(gè)空列表。

本章不會(huì)再展開(kāi)任何異常處理相關(guān)的話題。然而還有更多可說(shuō),我們將在第 19 章異常處理時(shí)重新探討這個(gè)主題。

練習(xí)

  1. 盡管我們已經(jīng)編寫(xiě)了一個(gè)可移植 namesMatching 函數(shù),這個(gè)函數(shù)使用了我們的大小寫(xiě)敏感的 globToRegex 函數(shù)。嘗試在不改變其類型簽名的前提下,使 namesMatching 在 Unix 下大小寫(xiě)敏感,在 Windows 下大小寫(xiě)不敏感。

提示:查閱一下 System.FilePath 的文檔,其中有一個(gè)變量可以告訴我們程序是運(yùn)行在類 Unix 系統(tǒng)上還是在 Windows 系統(tǒng)上。

  1. 如果你在使用類 Unix 系統(tǒng),查閱 System.Posix.Files 模塊的文檔,看是否能找到一個(gè) doesNameExist 的替代品。
    • 通配符,僅匹配一個(gè)單獨(dú)目錄中的名字。很多 shell 可以提供擴(kuò)展通配符語(yǔ)法, ,其將在所有目錄中進(jìn)行遞歸匹配。舉個(gè)例子,.c 意為 “在當(dāng)前目錄及其任意深度的子目錄下匹配一個(gè) .c 結(jié)尾的文件名”。實(shí)現(xiàn) ** 通配符匹配。

通過(guò) API 設(shè)計(jì)進(jìn)行錯(cuò)誤處理

向 globToRegex 傳入一個(gè)畸形的正則表達(dá)式未必會(huì)是一場(chǎng)災(zāi)難。用戶的表達(dá)式可能會(huì)有輸入錯(cuò)誤,這時(shí)我們更希望得到有意義的報(bào)錯(cuò)信息。

當(dāng)這類問(wèn)題出現(xiàn)時(shí),調(diào)用 error 函數(shù)會(huì)有很激烈的反應(yīng)(其結(jié)果在 Q: 1 這個(gè)練習(xí)中探索過(guò)。)。 error 函數(shù)會(huì)拋出一個(gè)異常。純函數(shù)式的 Haskell 代碼無(wú)法處理異常,所以控制流會(huì)突破我們的純函數(shù)代碼直接交給處于距離最近一層 IO 中并且安裝有合適的異常處理器的調(diào)用者。如果沒(méi)有安裝異常處理器, Haskell 運(yùn)行時(shí)的默認(rèn)動(dòng)作是終結(jié)我們的程序(如果是在 ghci 中,則會(huì)打出一條令人不快的錯(cuò)誤信息。)

所以,調(diào)用 error 有點(diǎn)像是拉下了戰(zhàn)斗機(jī)的座椅彈射手柄。我們從一個(gè)無(wú)法優(yōu)雅處理的災(zāi)難性場(chǎng)景中逃離,而等我們著地時(shí)會(huì)撒出很多燃燒著的殘骸。

我們已經(jīng)確定了 error 是為災(zāi)難情場(chǎng)景準(zhǔn)備的,但我們?nèi)耘f在 globToRegex 中使用它?;蔚妮斎雽⒈痪芙^,但不會(huì)導(dǎo)致大問(wèn)題。處理這種情況有更好的方式嗎?

Haskell 的類型系統(tǒng)和庫(kù)來(lái)救你了!我們可以使用內(nèi)置的 Either 類型,在 globToRegex 函數(shù)的類型簽名中描述失敗的可能性。

-- file: ch08/GlobRegexEither.hs
type GlobError = String

globToRegex :: String -> Either GlobError String

globToRegex 的返回值將為兩種情況之一,或者為 Left"出錯(cuò)信息" 或者為 Right"一個(gè)合法正則表達(dá)式" 。這種返回值類型,強(qiáng)制我們的調(diào)用者處理可能出現(xiàn)的錯(cuò)誤。(你會(huì)發(fā)現(xiàn)這是 Haskell 代碼中 Either 類型最廣泛的用途。)

練習(xí)

  1. 編寫(xiě)一個(gè)使用上面那種類型簽名的 globToRegex 版本。
  2. 改變 namesMatching 的類型簽名,使其可以處理畸形的正則表達(dá)式,并使用它重寫(xiě) globToRegex 函數(shù)。

Tip

你會(huì)發(fā)現(xiàn)牽扯到的工作量大得驚人。別怕,我們將在后面的章節(jié)介紹更多簡(jiǎn)單老練的處理錯(cuò)誤的方式。

讓我們的代碼工作

namesMatching 函數(shù)本身并不是很令人興奮,但它是一個(gè)很有用的構(gòu)建模塊。將它與稍多點(diǎn)的函數(shù)組合在一起,就會(huì)讓我們做出有趣的東西。

這里有個(gè)例子。定義一個(gè) renameWith 函數(shù),并不簡(jiǎn)單的重命名一個(gè)文件,取而代之,對(duì)文件名執(zhí)行一個(gè)函數(shù),并將返回值作為新的文件名。

-- file: ch08/Useful.hs
import System.FilePath (replaceExtension)
import System.Directory (doesFileExist, renameDirectory, renameFile)
import Glob (namesMatching)

renameWith :: (FilePath -> FilePath)
           -> FilePath
           -> IO FilePath

renameWith f path = do
    let path' = f path
    rename path path'
    return path'

我們?cè)僖淮瓮ㄟ^(guò)一個(gè)輔助函數(shù)使用 System.Directory 中難看的文件/目錄函數(shù)

-- file: ch08/Useful.hs
rename :: FilePath -> FilePath -> IO ()

rename old new = do
    isFile <- doesFileExist old
    let f = if isFile then renameFile else renameDirectory
    f old new

System.FilePath 模塊提供了很多有用的函數(shù)用于操作文件名。這些函數(shù)洽好漏過(guò)了我們的 renameWith 和 namesMatching 函數(shù),所以我們可以通過(guò)將他們組合起來(lái)的方式來(lái)快速的創(chuàng)建新函數(shù)。例如,這個(gè)簡(jiǎn)潔的函數(shù)修改了 C++ 源碼文件的后綴名。

-- file: ch08/Useful.hs
cc2cpp =
  mapM (renameWith (flip replaceExtension ".cpp")) =<< namesMatching "*.cc"

cc2cpp 函數(shù)使用了幾個(gè)我們已經(jīng)見(jiàn)過(guò)多次的函數(shù)。 flip 函數(shù)接受另一個(gè)函數(shù)作為參數(shù),交換其參數(shù)的順序(可以在 ghci 中調(diào)查 replaceExtension 的類型以了解詳情)。 =<< 函數(shù)將其右側(cè)動(dòng)作的結(jié)果喂給其左側(cè)的動(dòng)作。

練習(xí)

  1. Glob 模式解釋起來(lái)很簡(jiǎn)單,用 Haskell 可以很容易的直接寫(xiě)出其匹配器,正則表達(dá)式則不然。試一下編寫(xiě)正則匹配。
以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

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

編程獅公眾號(hào)