到目前為止,你已經定義過一些函式,不過,也一定遇過一些名稱衝突問題,例如,若在 .hs 中直接自定義一個 length
函式並進行呼叫,在編譯時就會發生「Ambiguous occurrence」的編譯錯誤:
之前你有使用過一些現成可用的函式,像是 filter
、map
等,這些都是 Haskell 預先從 Prelude
模組匯入(import),而 length
函式也是其中之一,如果你沒有為函式等名稱定義模組,那麼它們都會是 Main
模組的一部份,因此,當你想使用 length
這個函式時,編譯器就困惑了,你想使用的到底是 Prelude.length
?還是 Main.length
呢?
之前為了避免這類名稱衝突問題,都特意使用了 length'
這樣的名稱來迴避,現在,你可以自定義模組來解決這類問題。
自定義模組
在〈Haskell Tutorial(16)Record 語法、Type 同義詞〉中,自定義了 Map
型態,以及 fromList
、findValue
等函式,這邊就為它們定義一個 Map
模組好了,首先建立一個 Map.hs 檔案,內容為:
module Map
(
Map,
empty,
fromList,
findValue
) where
data Map k v = Empty | Cm (k, v) (Map k v)
empty = Empty
fromList :: [(k, v)] -> Map k v
fromList [] = Empty
fromList (x:xs) = Cm x (fromList xs)
findValue :: (Eq k) => k -> Map k v -> Maybe v
findValue key Empty = Nothing
findValue key (Cm (k, v) xm) =
if key == k then Just v else findValue key xm
模組一開始使用 module
來定義,如果你希望模組名稱為 Map
,那麼需要將 .hs 檔案的主檔名也取為相同名稱,接著括號中定義了模組可匯出的名稱有哪些,當其他人使用你的模組時,只有這邊定義的名稱才會被看見。
因此,注意到這邊,我只匯出了 Map
型態,但是沒有匯出 Empty
、Cm
這兩個值建構式,這麼做的目的是,其他人不再能使用值建構式來建立 Map
值,只能使用我提供的 fromList
來建立,也就是說,我隱藏了 Map
值的建立細節。
(這有點像是 Java 中將建構式設定為 private
後,提供一個工廠方法來建立類別實例。)
如果想要連同值建構式一同匯出,可以如下:
module Map
(
Map(Empty, Cm),
empty,
fromList,
findValue
) where
...
匯入模組
接下來,可以建立另一個 .hs 檔案來使用以上自定義的 Map
模組,像是:
import Map
main = do
let db = fromList [("Justin", "123456"), ("Monica", "654321")]
print $ findValue "Justin" db
如果 Main.hs、Map.hs 位於同一個目錄,你可以直接以指令 ghc Main.hs
編譯,這會連同 Map.hs 一同編譯,如果你的 Map.hs 位於其他目錄,你可以使用 ghc -idir1:dir2:dir3 Main.hs
進行編譯,其中 dir1 等是你的目錄名稱。
(有關 GHC 更多使用方式,可參考 各版本 GHC 的使用手冊。)
import module
語法,會讓指定的 module 中有匯出的名稱為可見,因此上頭你可以直接使用 fromList
、findValue
名稱。如果你只想要匯入其中幾個函式,可以使用 import module (name1, name2)
的格式,那麼就只有 module 匯出的 name1、name2 是可見的,如果你想要匯入大部份名稱,但隱藏其中幾個名稱,則可以使用 import module hiding (name1, name2)
的格式。
如果你想要將 Prelude
中某幾個預導入的函式隱藏起來,可以使用 import Prelude hiding (name1, name2)
,這樣,你就不用像之前那樣,特地迴避某個函式命名了。
如果你想要在匯入模組之後,保留模組名稱作為名稱空間,可以使用 import qualified module
,例如:
import qualified Map
main = do
let db = Map.fromList [("Justin", "123456"), ("Monica", "654321")]
print $ Map.findValue "Justin" db
你也可以使用其他名稱作為名稱空間,只要加上 as
來命名:
import qualified Map as M
main = do
let db = M.fromList [("Justin", "123456"), ("Monica", "654321")]
print $ M.findValue "Justin" db
如上所示,由於使用了 import qualified Map as M
,原 Map
模組中的名稱,現在可使用 M 作為名稱前置,你也可以在 import qualified
時結合 ()
或 hiding
,來達到想要的名稱空間管理效果,例如,import qualified Map (fromList, findValue)
或 import qualified Map as Map hiding (fromList, findValue)
。
關於 Main 模組
如先前談到的,對於一個原始碼,如果你沒有定義模組,就是屬於 Main
模組,即使你沒有直接定義出來,以最簡單的這個程式來說:
main = putStrLn "哈囉!世界!"
你也可以明確定義出 Main
模組:
module Main where
main = putStrLn "Hello, world!"
分層模組
隨著模組的越來越多,需要管理的名稱空間就會越來越多,檔案也會越來越多,單層結構的模組就會不敷使用,你可以建立分層模組,例如,若要將上頭的 fromList
、findValue
,改置於 Util.Map
模組之中,那麼 Map.hs 可以修改如下:
module Util.Map
(
Map,
empty,
fromList,
findValue
) where
data Map k v = Empty | Cm (k, v) (Map k v)
-- 其餘同上
由於 module
定義了名稱為 Util.Map,你的 Map.hs 也必須置於 Util 目錄底下,要使用這個模組,你可以如下撰寫 Main.hs:
import Util.Map
main = do
let db = fromList [("Justin", "123456"), ("Monica", "654321")]
print $ findValue "Justin" db
在使用 ghc
指令編譯 Main.hs 時,可以使用 -rdir1:dir2:dir2
來指定分層模組的起始目錄,例如指定 ghc -iMyModule Main.hs
,那麼,在 MyModule 目錄下,就要包括你的 Util 目錄。