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