跳到内容

另一个 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 样式的脚本中,空行是必要的。

华夏公益教科书