跳至內容

Haskell/構建詞彙

來自華夏公益教科書,開放的書籍,開放的世界

本章將作為一段插曲,提供一些關於學習和使用 Haskell 的建議。我們將討論獲取函式詞彙的重要性,以及本書和其他資源如何提供幫助。但是,首先我們需要了解函式組合。

函式組合

[編輯 | 編輯原始碼]

函式組合意味著將一個函式應用於一個值,然後將另一個函式應用於結果。考慮這兩個函式

示例:簡單函式

f x = x + 3
square x = x ^ 2

我們可以用兩種不同的方式組合它們,這取決於我們首先應用哪個函式

Prelude> square (f 1)
16
Prelude> square (f 2)
25
Prelude> f (square 1)
4
Prelude> f (square 2)
7

內部函數週圍的括號是必要的;否則,直譯器會認為你試圖獲取 square ff square 的值;這兩種情況都會導致型別錯誤。

兩個函式的組合會產生一個獨立的函式。如果我們經常應用 f 然後平方(反之亦然),我們應該為結果組合生成一個新的變數名

示例:組合函式

squareOfF x = square (f x)

fOfSquare x = f (square x)

還有一種巧妙的方法來編寫組合函式。它使用 (.),函式組合運算子,就像在兩個函式之間加一個句點一樣簡單

示例:使用 (.) 組合函式

squareOfF x = (square . f) x

fOfSquare x = (f . square) x

請注意,函式仍然是從右到左應用的,因此 g(f(x)) == (g . f) x(.) 模仿數學運算子 ,它以相同的方式工作: .

順便說一句,我們的函式定義實際上是數學方程式,所以我們可以取

squareOfF x = (square . f) x

並從兩邊消去 x,留下

squareOfF = square . f

我們將在後面學習更多關於這種沒有顯示引數的函式的情況。現在,請理解我們可以簡單地用我們定義的變數名替換任何組合函式的情況。

需要詞彙

[編輯 | 編輯原始碼]

Haskell 使編寫組合函式和定義變數變得簡單,因此我們最終得到相對簡單、優雅和表達力強的程式碼。當然,為了使用函式組合,我們首先需要有函式可以組合。雖然我們自己編寫的函式將始終可用,但每個 GHC 安裝都附帶大量庫(即打包的程式碼),這些庫提供了用於許多常見任務的函式。因此,有效的 Haskell 程式設計師需要對基本庫有一定程度的瞭解。至少,你應該知道如何在需要時在庫中找到有用的函式。

僅憑我們將在 遞迴 章中介紹的 Haskell 語法,原則上我們就有足夠的知識來編寫幾乎任何我們想要的列表操作程式。但是,僅使用這些基礎知識來編寫完整的程式將非常低效,因為我們最終將重新編寫標準庫的大部分內容。因此,我們今後的大部分學習將涉及學習和理解 Haskell 社群已經構建的這些寶貴的工具。

Prelude 和庫

[編輯 | 編輯原始碼]

以下是一些關於 Haskell 庫的基本事實

首先,Prelude 是預設載入到每個 Haskell 程式中的核心庫。這個無所不在的庫提供了一組有用的函式、類和型別。我們在這些入門章節中介紹了 Prelude 型別和函式。

GHC 提供了一組龐大的核心庫,具有各種工具,但只有 Prelude 會自動載入。其他庫是模組,可供你的程式匯入。稍後,我們將解釋模組的工作原理。現在,只需知道你的原始檔需要在頂部附近新增幾行來匯入任何所需的模組。例如,要使用 Data.List 模組中的 permutations 函式,請在你的 .hs 檔案頂部新增 import Data.List 行。以下是一個完整的原始檔示例

示例:在原始檔中匯入模組

import Data.List

testPermutations = permutations "abc"

對於快速的 GHCi 測試,只需在命令列中輸入 :m +Data.List 來載入該模組。

Prelude> :m +Data.List
Prelude Data.List> :t permutations
permutations :: [a] -> [[a]]

一個例子

[編輯 | 編輯原始碼]

在繼續之前,讓我們看一下(我們承認有點戲劇性的)熟悉 Prelude 中的一些基本函式可以為我們帶來的示例。[1] 假設我們需要一個函式,它接受一個由空格分隔的單片語成的字串,並返回該字串,其中單詞的順序反轉,例如 "Mary had a little lamb" 變成 "lamb little a had Mary"。我們可以使用我們已經介紹過的基本知識以及即將介紹的遞迴章節中的一些見解來解決這個問題。下面是一個混亂、複雜的解決方案。不要盯著它看太久!

示例:這裡有巨龍

monsterRevWords :: String -> String
monsterRevWords input = rejoinUnreversed (divideReversed input)
    where
    divideReversed s = go1 [] s
        where
        go1 divided [] = divided
        go1 [] (c:cs)
            | testSpace c = go1 [] cs
            | otherwise   = go1 [[]] (c:cs)
        go1 (w:ws) [c]
            | testSpace c = (w:ws)
            | otherwise   = ((c:w):ws)
        go1 (w:ws) (c:c':cs)
            | testSpace c =
                if testSpace c'
                    then go1 (w:ws) (c':cs)
                    else go1 ([c']:w:ws) cs
            | otherwise = go1 ((c:w):ws) (c':cs)
    testSpace c = c == ' '
    rejoinUnreversed [] = []
    rejoinUnreversed [w] = reverseList w
    rejoinUnreversed strings = go2 (' ' : reverseList newFirstWord) (otherWords)
        where
        (newFirstWord : otherWords) = reverseList strings
        go2 rejoined ([]:[]) = rejoined
        go2 rejoined ([]:(w':ws')) = go2 (rejoined) ((' ':w'):ws')
        go2 rejoined ((c:cs):ws) = go2 (c:rejoined) (cs:ws)
    reverseList [] = []
    reverseList w = go3 [] w
        where
        go3 rev [] = rev
        go3 rev (c:cs) = go3 (c:rev) cs

這個東西存在太多問題;所以讓我們只考慮其中三個

  • 要檢視 monsterRevWords 是否按預期執行,你可以相信我們的話,對各種可能的輸入進行全面測試,或者嘗試理解它並獲得可怕的頭痛(請不要這樣做)。
  • 此外,如果我們編寫一個如此醜陋的函式,並且以後不得不修復錯誤或稍微修改它,[2] 我們將面臨一段可怕的時光。
  • 最後,我們至少有一個容易發現的潛在問題:如果你再仔細看看定義,在中間部分有一個 testSpace 輔助函式,它檢查一個字元是否是空格。然而,該測試僅包括常見的空格字元(即 ' '),而沒有其他空白字元(製表符、換行符等)。[3]

如果我們使用以下 Prelude 函式,我們可以做得比上面的垃圾程式碼好得多

  • words,它可靠地將字串分解為以空格分隔的單詞,返回一個字串列表;
  • reverse,它反轉列表(順便說一下,這就是上面 reverseList 所做的);以及
  • unwords,它與 words 相反;

然後函式組合意味著我們的問題立即得到解決。

示例:revWords 以 Haskell 風格完成

revWords :: String -> String
revWords input = (unwords . reverse . words) input

這很短、簡單、易讀,而且(因為 Prelude 是可靠的)沒有錯誤。[4] 所以,任何時候你正在編寫的程式開始看起來像 monsterRevWords,請環顧四周,伸手去拿你的工具箱——庫。

本書對庫的使用

[編輯 | 編輯原始碼]

在上述嚴厲警告之後,你可能期望我們繼續深入研究標準庫。然而,初學者學習路線的目的是以概念性、可讀性和合理簡潔的方式涵蓋 Haskell 的功能。系統地研究庫不會幫助我們,但我們將根據我們涵蓋的每個概念,適當地介紹庫中的函式。

  • 在初級 Haskell 部分,一些練習(主要是關於列表處理的練習)涉及編寫 Prelude 函式的等效定義。對於你完成的每個練習,你的工具庫中將新增一個新的函式。
  • 我們偶爾會介紹更多庫函式;可能是在例子中,或者只是順便提一下。每當我們這樣做時,花點時間測試該函式並進行一些實驗。記住將我們在型別基礎中提到的關於型別的習慣性好奇心擴充套件到函式本身。
  • 雖然前幾章關係緊密,但本書後面的部分更加獨立。 實踐中的 Haskell 包含關於分層庫的章節,並且它們的大部分內容可以在完成初級 Haskell 後不久就理解。
  • 隨著我們進入初學者學習路線的後面部分,我們將討論的概念(特別是單子)將自然而然地引導我們探索核心庫的重要部分。

其他資源

[edit | edit source]
  • 首先,所有模組都具有基本文件。你現在可能還沒有準備好直接閱讀它,但我們會做到。你可以線上閱讀Prelude 規範以及與 GHC 捆綁在一起的庫的文件,它具有不錯的導航功能,只需單擊一下即可訪問原始碼。
  • Hoogle 是一個透過文件搜尋的絕佳方法,對初學者友好。它涵蓋了大量的庫。你可以按函式名(比如,length)或按型別(“從列表到 Int 的函式”,你將寫成 [a]->Int)進行搜尋。這種第二種型別的搜尋可以幫助你找到非常具體的函式,並避免重複造輪子。
  • 除了 GHC 中包含的庫之外,還有一個龐大的庫生態系統,可以透過Hackage 獲得,並且可以使用名為cabal 的工具進行安裝。 Hackage 網站提供了其庫的文件。我們不會在初學者學習路線中嘗試使用核心庫之外的庫,但你當然應該在開始自己的專案後使用 Hackage。
  • 在適當的情況下,我們將提供指向其他有用學習資源的指標,尤其是在我們轉向中級和高階主題時。

筆記

  1. 此處的示例受 HaskellWiki 中的簡單 Unix 工具 演示的啟發。
  2. 合著者注:“後面再說?我半個小時前寫的,現在還沒完全搞清楚……”
  3. 檢查字元是否為空格的可靠方法是使用 isSpace 函式,該函式位於 Data.Char 模組中。
  4. 如果你想知道,Prelude 或 Data.List 中的許多其他函式可以幫助使 monsterRevWords 更簡潔一些——舉幾個例子:(++)concatgroupByintersperse——但是沒有一種使用方式可以與上面的單行程式碼相比。
華夏公益教科書