跳到內容

Haskell/模組

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

模組是組織 Haskell 程式碼的主要方式。當使用 import 語句將庫函式引入作用域時,我們順便提到了它們。除了讓我們更好地使用庫之外,對模組的瞭解將有助於我們塑造自己的程式並建立獨立的程式,這些程式可以獨立於 GHCi 執行(順便說一下,這是下一章的主題,獨立程式)。

Haskell 模組[1] 是將一組相關功能分組到單個包中並管理可能具有相同名稱的不同函式的有用方法。模組定義是 Haskell 檔案中首先要寫的內容。

一個基本的模組定義看起來像

module YourModule where

請注意

  1. 模組名稱以大寫字母開頭;
  2. 每個檔案只包含一個模組。

檔名稱是模組名稱加上.hs副檔名。任何點 '.' 在模組名稱中將被更改為目錄。[2] 所以模組YourModule將在檔案中YourModule.hs而模組Foo.Bar將在檔案中Foo/Bar.hsFoo\Bar.hs. 由於模組名稱必須以大寫字母開頭,因此檔名稱也必須以大寫字母開頭。

模組本身可以從其他模組匯入函式。也就是說,在模組宣告和你程式碼的其餘部分之間,你可以包含一些匯入宣告,例如

import Data.Char (toLower, toUpper) -- import only the functions toLower and toUpper from Data.Char

import Data.List -- import everything exported from Data.List
 
import MyModule -- import everything exported from MyModule

匯入的資料型別由其名稱指定,後面括號中列出了匯入的建構函式。例如

import Data.Tree (Tree(Node)) -- import only the Tree data type and its Node constructor from Data.Tree

如果你匯入了一些具有重疊定義的模組怎麼辦?或者如果你匯入了一個模組但想自己覆蓋一個函式?有三種方法可以處理這些情況:限定匯入、隱藏定義和重新命名匯入。

限定匯入

[編輯 | 編輯原始碼]

假設 MyModule 和 MyOtherModule 都對remove_e有定義,它從字串中刪除所有 e 例項。但是,MyModule 只刪除小寫字母 e,而 MyOtherModule 則刪除大小寫字母。在這種情況下,以下程式碼是模稜兩可的

import MyModule
import MyOtherModule

-- someFunction puts a c in front of the text, and removes all e's from the rest
someFunction :: String -> String
someFunction text = 'c' : remove_e text

不清楚哪個remove_e是針對!為了避免這種情況,使用 **qualified** 關鍵字

import qualified MyModule
import qualified MyOtherModule

someFunction text = 'c' : MyModule.remove_e text -- Will work, removes lower case e's
someOtherFunction text = 'c' : MyOtherModule.remove_e text -- Will work, removes all e's
someIllegalFunction text = 'c' : remove_e text -- Won't work as there is no remove_e defined

在後面的程式碼片段中,沒有名為remove_e的函式可用。當我們進行限定匯入時,所有匯入的值都包含模組名稱作為字首。順便說一下,你也可以使用相同的這些字首,即使你做了常規匯入(在我們的例子中,MyModule.remove_e即使不包含 "qualified" 關鍵字也能正常工作)。

注意

限定名稱(如 MyModule.remove_e)和函式組合運算子 (.) 之間存在歧義。寫 reverse.MyModule.remove_e 很可能會讓你的 Haskell 編譯器感到困惑。一個解決方案是風格:始終使用空格進行函式組合,例如 reverse . remove_eJust . remove_e 甚至 Just . MyModule.remove_e


隱藏定義

[編輯 | 編輯原始碼]

現在假設我們想匯入兩個模組MyModuleMyOtherModule,但我們確定要刪除所有 e,而不僅僅是小寫 e。新增MyOtherModule在每次呼叫remove_e之前將變得非常繁瑣。我們不能僅僅排除remove_eMyModule?

import MyModule hiding (remove_e)
import MyOtherModule

someFunction text = 'c' : remove_e text

?這行得通,因為匯入行上的 **hiding** 關鍵字。在 "hiding" 關鍵字後面的任何內容都不會被匯入。透過使用括號和逗號分隔列出它們,隱藏多個專案

import MyModule hiding (remove_e, remove_f)

請注意,代數資料型別和類型別名無法隱藏。它們總是被匯入。如果你在多個匯入的模組中定義了資料型別,則必須使用限定名稱。

重新命名匯入

[編輯 | 編輯原始碼]

這實際上不是一種允許覆蓋的技術,但它通常與 qualified 標誌一起使用。想象一下

import qualified MyModuleWithAVeryLongModuleName

someFunction text = 'c' : MyModuleWithAVeryLongModuleName.remove_e text

尤其是在使用 qualified 時,這會變得令人厭煩。我們可以使用 **as** 關鍵字改進它

import qualified MyModuleWithAVeryLongModuleName as Shorty

someFunction text = 'c' : Shorty.remove_e text

這允許我們使用Shorty而不是MyModuleWithAVeryLongModuleName作為匯入函式的字首。這種重新命名對限定匯入和常規匯入都有效。

只要沒有衝突的專案,我們就可以匯入多個模組並將它們重新命名為相同

import MyModule as My
import MyCompletelyDifferentModule as My

在這種情況下,中的函式MyModule和中的函式MyCompletelyDifferentModule都可以以 My 為字首。

將重新命名與限制匯入相結合

[編輯 | 編輯原始碼]

有時用相同的模組對匯入指令使用兩次會很方便。一個典型的場景如下

import qualified Data.Set as Set
import Data.Set (Set, empty, insert)

這透過別名 "Set" 提供了對 Data.Set 模組的所有訪問,並且還允許你訪問一些選定的函式(empty、insert 和建構函式),而無需使用 "Set" 字首。

在本文開頭的示例中,使用了 "匯入 *從 MyModule 匯出* 的所有內容" 的說法。[3] 這提出了一個問題。我們如何決定哪些函式被匯出,哪些函式保持 "內部"?方法如下

module MyModule (remove_e, add_two) where

add_one blah = blah + 1

remove_e text = filter (/= 'e') text

add_two blah = add_one . add_one $ blah

在這種情況下,只有remove_eadd_two被匯出。雖然add_two被允許使用add_one,但匯入MyModule的模組中的函式不能直接使用add_one,因為它沒有被匯出。

資料型別匯出規範的寫法類似於匯入。你命名型別,並在括號中列出建構函式

module MyModule2 (Tree(Branch, Leaf)) where

data Tree a = Branch {left, right :: Tree a} 
            | Leaf a

在這種情況下,模組宣告可以改寫為 "MyModule2 (Tree(..))",宣告所有建構函式都被匯出。

維護匯出列表是一個良好的習慣,不僅因為它減少了名稱空間汙染,而且因為它使某些 編譯時最佳化 成為可能,否則這些最佳化將不可用。

筆記

  1. 有關模組系統的更多詳細資訊,請參見 Haskell 報告
  2. 在 Haskell98 中,Haskell 2010 之前 Haskell 的最後一個標準化版本,模組系統相當保守,但最近的常見做法是使用分層模組系統,使用句號劃分名稱空間。
  3. 一個模組可以匯出它匯入的函式。相互遞迴模組是可能的,但需要 一些特殊處理
華夏公益教科書