定義與使用模組

February 9, 2022

到目前為止,你已經定義過一些函式,也可能遇過一些名稱衝突問題,例如,若在 .hs 中直接自定義一個 length 函式:

length :: [a] -> Int
length [] = 0
length (_:xs) = 1 + (length $ tail xs)

編譯時就會發生「Ambiguous occurrence」的錯誤:

    Ambiguous occurrence ‘length’
    It could refer to
       either ‘Prelude.length’,
              imported from ‘Prelude’ at test.hs:1:1
              (and originally defined in ‘Data.Foldable’)
           or ‘Main.length’, defined at test.hs:2:1
  |
3 | length (_:xs) = 1 + (length $ tail xs)
  |                      ^^^^^^

之前使用過一些標準函式,像是 filtermap 等,這些是 Haskell 預先從 Prelude 模組匯入(import)的函式,length 函式也是其中之一。

如果沒有為自訂名稱或函式等定義模組,那麼它們會是 Main 模組的一部份,因此想使用 length 這個函式時,編譯器就困惑了,你想使用的到底是 Prelude.length?還是 Main.length 呢?

自定義模組

來試著定義一個 List 模組,於其中自訂義 List 型態,並實現 lengthmap 等函式:

module List
(
    List(Empty, Con),
    length,
    map
) where

import Prelude hiding (length, map)

data List a = Empty | Con a (List a) deriving Show

length :: List a -> Int
length Empty = 0
length (Con _ xs) = 1 + (length xs)

map :: (a -> a) -> List a -> List a
map _ Empty = Empty
map mapper (Con x xs) = Con (mapper x) (map mapper xs)

Haskell 使用 module 定義模組,如果希望模組名稱為 List,那麼 .hs 檔案的主檔名也要取為相同名稱,接著括號中定義了模組可匯出的名稱有哪些,當其他人使用你的模組時,只有這邊定義的名稱才會被看見。

如果你自定義了資料型態,若想要其他人能使用值建構式,記得也要在括號中匯出;如果不想要別人使用值建構式,只能使用你提供的函式來建立值,可以不用匯出,如此一來,可以隱藏值的建立與模式匹配等細節。

如果想將 Prelude 中預導入的函式隱藏起來,可以使用 import Prelude hiding (name1, name2),這樣就不用迴避 lengthmap 等內建函式的命名。

匯入模組

接下來,可以建立另一個 .hs 檔案來使用以上自定義的 Map 模組,像是:

import Prelude hiding (length, map)
import List

main = do
    let lt = Con 1 $ Con 2 $ Con 3 Empty
    print $ length lt             -- 顯示 3
    print $ map (\x -> x * 2) lt  -- 顯示 Con 2 (Con 4 (Con 6 Empty))

如果 List.hs 位於同一個目錄,可以直接以指令 ghc Main.hs 編譯,這會連同 List.hs 一同編譯,如果 List.hs 位於其他目錄,可以使用 ghc -idir1:dir2:dir3 Main.hs 進行編譯,其中 dir1 等是目錄名稱。

import module 語法,可讓 module 匯出的名稱全為可見,因此上面的範例可以直接使用 lengthmap 等名稱,為了不與內建的 lengthmap 衝突,一開始也撰寫了 import Prelude hiding (length, map)

如果只想要匯入其中幾個函式,可以使用 import module (name1, name2) 的格式,那麼就只有 module 匯出的 name1name2 是可見的,如果想要匯入大部份名稱,但隱藏其中幾個名稱,就是使用方才看過的 import module hiding (name1, name2) 格式。

如果想在匯入模組後,保留模組名稱作為名稱空間,可以使用 import qualified module,例如:

import qualified List

main = do
    let lt = List.Con 1 $ List.Con 2 $ List.Con 3 List.Empty
    print $ List.length lt             -- 顯示 3
    print $ List.map (\x -> x * 2) lt  -- 顯示 Con 2 (Con 4 (Con 6 Empty))

也可以使用其他名稱作為名稱空間,只要加上 as 來命名:

import qualified List as Lt

main = do
    let lt = Lt.Con 1 $ Lt.Con 2 $ Lt.Con 3 Lt.Empty
    print $ Lt.length lt             -- 顯示 3
    print $ Lt.map (\x -> x * 2) lt  -- 顯示 Con 2 (Con 4 (Con 6 Empty))

如上所示,由於使用了 import qualified List as Lt,原 Lt 模組中的名稱,現在可使用 LtLt 作為名稱前置,也可以在 import qualified 時結合 ()hiding,來達到想要的名稱空間管理效果,例如,import qualified List (length, map)import qualified List as Lt hiding (length, map)

關於 Main 模組

如果沒有定義模組,就是屬於 Main 模組,以最簡單的這個程式來說:

main = putStrLn "哈囉!世界!"

也可以明確定義出 Main 模組:

module Main where
main = putStrLn "Hello, world!"

分層模組

隨著模組越來越多,需要管理的名稱空間會越來越多,檔案也越來越多,單層結構的模組就會不敷使用,可以建立分層模組,例如,若要將上頭的 Listlengthmap,改置於 Util.List 模組之中,那麼 List.hs 可以修改如下:

module Utill.List
(
    List(Empty, Con),
    length,
    map
) where

import Prelude hiding (length, map)

-- 其餘同上

由於 module 定義了名稱 Util.List,你的 List.hs 也必須置於 Util 目錄底下,要使用這個模組,可以如下撰寫 Main.hs:

import qualified Util.List as Lt

main = do
    let lt = Lt.Con 1 $ Lt.Con 2 $ Lt.Con 3 Lt.Empty
    print $ Lt.length lt             -- 顯示 3
    print $ Lt.map (\x -> x * 2) lt  -- 顯示 Con 2 (Con 4 (Con 6 Empty))

在使用 ghc 指令編譯 Main.hs 時,可以使用 -rdir1:dir2:dir2 來指定分層模組的起始目錄,例如指定 ghc -iMyModule Main.hs,那麼,在 MyModule 目錄下,就要包括你的 Util 目錄。

分享到 LinkedIn 分享到 Facebook 分享到 Twitter