跳转到内容

另一种 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 风格的脚本中,空行不是必需的。

华夏公益教科书