另一种 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.
- 我们可以使用作为关键字
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 风格的文字式程序中,注释是默认的,代码以一个领先的“大于”符号(“>
”)引入。其他一切保持不变。例如,我们的 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 风格的脚本中,空行不是必需的。