定義與使用模組
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)
| ^^^^^^
之前使用過一些標準函式,像是 filter
、map
等,這些是 Haskell 預先從 Prelude
模組匯入(import)的函式,length
函式也是其中之一。
如果沒有為自訂名稱或函式等定義模組,那麼它們會是 Main
模組的一部份,因此想使用 length
這個函式時,編譯器就困惑了,你想使用的到底是 Prelude.length
?還是 Main.length
呢?
自定義模組
來試著定義一個 List
模組,於其中自訂義 List
型態,並實現 length
、map
等函式:
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)
,這樣就不用迴避 length
、map
等內建函式的命名。
匯入模組
接下來,可以建立另一個 .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
匯出的名稱全為可見,因此上面的範例可以直接使用 length
、map
等名稱,為了不與內建的 length
、map
衝突,一開始也撰寫了 import Prelude hiding (length, map)
。
如果只想要匯入其中幾個函式,可以使用 import module (name1, name2)
的格式,那麼就只有 module
匯出的 name1
、name2
是可見的,如果想要匯入大部份名稱,但隱藏其中幾個名稱,就是使用方才看過的 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!"
分層模組
隨著模組越來越多,需要管理的名稱空間會越來越多,檔案也越來越多,單層結構的模組就會不敷使用,可以建立分層模組,例如,若要將上頭的 List
、length
、map
,改置於 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 目錄。