跳轉到內容

另一種 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. - 我們可以使用作為關鍵字


import qualified Cards as C

這些選項可以混合和匹配 - 例如,你可以在限定/作為匯入上提供明確的匯入列表。



分層匯入

[編輯 | 編輯原始碼]

雖然從技術上講不是 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

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

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 風格的指令碼中,空行不是必需的。

華夏公益教科書