跳到內容

另一個 Haskell 教程/模組

來自華夏公益教科書,開放的書籍,開放的世界
Haskell
另一個 Haskell 教程
前言
介紹
入門
語言基礎 (解決方案)
型別基礎 (解決方案)
IO (解決方案)
模組 (解決方案)
高階語言 (解決方案)
高階型別 (解決方案)
單子 (解決方案)
高階 IO
遞迴
複雜度

在 Haskell 中,程式子元件被劃分成模組。每個模組都位於它自己的檔案中,並且模組的名稱應該與檔名一致(當然,不包括“.hs”副檔名),如果你想在更大的程式中使用該模組。

例如,假設我正在編寫一個撲克遊戲。我可能希望有一個名為“Cards”的單獨模組來處理牌的生成、洗牌和發牌功能,然後在我的“Poker”模組中使用這個“Cards”模組。這樣,如果我以後想編寫一個二十一點程式,就不必重寫所有牌的程式碼;我可以簡單地匯入舊的“Cards”模組。


正如建議的那樣,假設我們正在編寫一個牌模組。我省略了實現細節,但假設我們的模組骨架看起來像這樣

module Cards
    where

data Card = ...
data Deck = ...

newDeck :: ... -> Deck
newDeck = ...

shuffle :: ... -> Deck -> Deck
shuffle = ...

-- 'deal deck n' deals 'n' cards from 'deck'
deal :: Deck -> Int -> [Card]
deal deck n = dealHelper deck n []

dealHelper = ...

在這段程式碼中,函式deal呼叫了一個輔助函式dealHelper。該輔助函式的實現高度依賴於你為CardDeck使用的確切資料結構,因此我們不希望其他人能夠呼叫該函式。為此,我們建立一個匯出列表,將其插入模組名稱宣告之後

module Cards ( Card(),
               Deck(),
               newDeck,
               shuffle,
               deal
             )
    where

...

在這裡,我們指定了模組匯出的確切函式,因此使用該模組的人將無法訪問我們的dealHelper函式。CardDeck後面的()指定我們匯出的是型別,而不是任何建構函式。例如,如果我們對Card的定義是

data Card = Card Suit Face
data Suit = Hearts
          | Spades
          | Diamonds
          | Clubs
data Face = Jack
          | Queen
          | King
          | Ace
          | Number Int

那麼我們模組的使用者將能夠使用型別為Card的東西,但無法構建他們自己的Card,也無法提取儲存在它們中的任何花色/牌面資訊。

如果我們希望模組的使用者能夠訪問所有這些資訊,我們必須在匯出列表中指定它

module Cards ( Card(Card),
               Suit(Hearts,Spades,Diamonds,Clubs),
               Face(Jack,Queen,King,Ace,Number),
               ...
             )
    where

...

如果要匯出具有許多建構函式的資料型別,這可能會很麻煩,所以如果你想匯出所有建構函式,只需寫(..),例如

module Cards ( Card(..),
               Suit(..),
               Face(..),
               ...
             )
    where

...

這將自動匯出所有建構函式。



模組匯入系統有一些特殊情況,但只要避免這些極端情況,你應該沒問題。假設,如前所述,你編寫了一個名為“Cards”的模組,並將其儲存在檔案“Cards.hs”中。你現在正在編寫你的撲克模組,你想匯入“Cards”模組中的所有定義。為此,你只需要寫


module Poker
    where

import Cards

這將使你能夠使用“Cards”模組匯出的任何函式、型別和建構函式。你可以簡單地透過它們在“Cards”模組中的名稱來引用它們(例如,newDeck),或者可以明確地將它們引用為從“Cards”匯入(例如,Cards.newDeck)。可能存在兩個模組匯出相同名稱的函式或型別的情況。在這些情況下,你可以匯入其中一個模組限定這意味著你將不再能夠簡單地使用newDeck格式,而是必須使用更長的Cards.newDeck格式,以消除歧義。如果你想以這種限定形式匯入“Cards”,你需要寫


import qualified Cards

避免函式定義重疊問題的另一種方法是從模組中匯入特定函式。假設我們知道我們想要從“Cards”中唯一匯入的函式是newDeck,我們可以透過編寫以下內容來僅匯入該函式

import Cards (newDeck)

另一方面,假設我們知道deal函式與另一個模組重疊,但我們不需要“Cards”版本的該函式。我們可以隱藏deal的定義並匯入其他所有內容,方法是編寫


import Cards hiding (deal)

最後,假設我們想將“Cards”作為限定模組匯入,但不想一直輸入Cards.,而是想輸入例如C. - 我們可以使用as關鍵字


import qualified Cards as C

這些選項可以混合使用——例如,你可以在限定/as 匯入上給出顯式匯入列表。



分層匯入

[編輯 | 編輯原始碼]

雖然從技術上講不是 Haskell 98 標準的一部分,但大多數 Haskell 編譯器都支援分層匯入。這旨在消除模組儲存目錄中的混亂。分層匯入允許你(在一定程度上)指定模組在目錄結構中的位置。例如,如果你在計算機上有一個“haskell”目錄,並且該目錄在你的編譯器的路徑中(請參閱你的編譯器說明,瞭解如何設定它;在 GHC 中是“-i”,在 Hugs 中是“-P”),那麼你可以在該目錄的子目錄中指定模組位置。

假設你沒有將“Cards”模組儲存在你的一般 haskell 目錄中,而是專門為此模組建立了一個名為“Cards”的目錄。則Cards.hs檔案的完整路徑為haskell/Cards/Cards.hs(或,對於 Windowshaskell\Cards\Cards.hs)。如果你將 Cards 模組的名稱更改為“Cards.Cards”,例如

module Cards.Cards(...)
    where

...

那麼你可以在任何模組中匯入它,無論該模組的目錄是什麼,方法是

import Cards.Cards

如果你開始限定匯入這些模組,我強烈建議使用as關鍵字縮短名稱,以便你可以編寫

import qualified Cards.Cards as Cards

... Cards.newDeck ...

而不是

import qualified Cards.Cards

... Cards.Cards.newDeck ...

這往往會很醜。



文學式與非文學式

[編輯 | 編輯原始碼]

文學式程式設計的想法相對簡單,但普及起來卻花了相當長的時間。當我們想到程式設計時,我們會認為程式碼是預設的輸入方式,而註釋是次要的。也就是說,我們編寫沒有特殊註釋的程式碼,但註釋用--{- ... -}進行註釋。文學式程式設計交換了這些先入為主的觀念。

Haskell 中有兩種型別的文學式程式;第一種使用所謂的 Bird 指令碼,第二種使用 LaTeX 樣式的標記。每種型別將在單獨的章節中討論。無論你使用哪種型別,文學式指令碼必須使用副檔名lhs而不是hs,以告知編譯器程式是用文學式風格編寫的。

Bird 指令碼

[編輯 | 編輯原始碼]

在 Bird 樣式的文學式程式中,註釋是預設的,程式碼以引導大於號 (“>”) 作為開頭。其他所有內容保持不變。例如,我們的 Hello World 程式將以 Bird 樣式編寫為

This is a simple (literate!) Hello World program.

> module Main
>     where

All our main function does is print a string:

> main = putStrLn "Hello World"

注意,程式碼行和“註釋”之間的空格是必要的(如果你缺少空格,你的編譯器可能會報錯)。當編譯或載入到直譯器中時,該程式將具有與檔案部分中的非文學式版本完全相同的屬性。

LaTeX 指令碼

[編輯 | 編輯原始碼]

LaTeX 是一種文字標記語言,在學術界非常流行,用於出版。如果你不熟悉 LaTeX,你可能不會覺得本節特別有用。

同樣,以 LaTeX 樣式編寫的文學式 Hello World 程式將如下所示

This is another simple (literate!) Hello World program.

\begin{code}
module Main
    where
\end{code}

All our main function does is print a string:

\begin{code}
main = putStrLn "Hello World"
\end{code}

在 LaTeX 樣式的指令碼中,空行是必要的。

華夏公益教科書