另一个 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。该辅助函数的实现高度依赖于你为Card和Deck使用的确切数据结构,因此我们不希望其他人能够调用该函数。为此,我们创建一个导出列表,将其插入模块名称声明之后
module Cards ( Card(),
Deck(),
newDeck,
shuffle,
deal
)
where
...
在这里,我们指定了模块导出的确切函数,因此使用该模块的人将无法访问我们的dealHelper函数。Card和Deck后面的()指定我们导出的是类型,而不是任何构造函数。例如,如果我们对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 样式的文学式程序中,注释是默认的,代码以引导大于号 (“>”) 作为开头。其他所有内容保持不变。例如,我们的 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 样式编写的文学式 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 样式的脚本中,空行是非必要的。
