跳到内容

Haskell/模块

来自维基教科书,开放的书籍,为开放的世界

模块是组织 Haskell 代码的主要方式。当使用 import 语句将库函数引入作用域时,我们顺便提到了它们。除了让我们更好地使用库之外,对模块的了解将有助于我们塑造自己的程序并创建独立的程序,这些程序可以独立于 GHCi 执行(顺便说一下,这是下一章的主题,独立程序)。

Haskell 模块[1] 是将一组相关功能分组到单个包中并管理可能具有相同名称的不同函数的有用方法。模块定义是 Haskell 文件中首先要写的内容。

一个基本的模块定义看起来像

module YourModule where

请注意

  1. 模块名称以大写字母开头;
  2. 每个文件只包含一个模块。

文件名称是模块名称加上.hs文件扩展名。任何点 '.' 在模块名称中将被更改为目录。[2] 所以模块YourModule将在文件中YourModule.hs而模块Foo.Bar将在文件中Foo/Bar.hsFoo\Bar.hs. 由于模块名称必须以大写字母开头,因此文件名称也必须以大写字母开头。

模块本身可以从其他模块导入函数。也就是说,在模块声明和你代码的其余部分之间,你可以包含一些导入声明,例如

import Data.Char (toLower, toUpper) -- import only the functions toLower and toUpper from Data.Char

import Data.List -- import everything exported from Data.List
 
import MyModule -- import everything exported from MyModule

导入的数据类型由其名称指定,后面括号中列出了导入的构造函数。例如

import Data.Tree (Tree(Node)) -- import only the Tree data type and its Node constructor from Data.Tree

如果你导入了一些具有重叠定义的模块怎么办?或者如果你导入了一个模块但想自己覆盖一个函数?有三种方法可以处理这些情况:限定导入、隐藏定义和重命名导入。

限定导入

[编辑 | 编辑源代码]

假设 MyModule 和 MyOtherModule 都对remove_e有定义,它从字符串中删除所有 e 实例。但是,MyModule 只删除小写字母 e,而 MyOtherModule 则删除大小写字母。在这种情况下,以下代码是模棱两可的

import MyModule
import MyOtherModule

-- someFunction puts a c in front of the text, and removes all e's from the rest
someFunction :: String -> String
someFunction text = 'c' : remove_e text

不清楚哪个remove_e是针对!为了避免这种情况,使用 **qualified** 关键字

import qualified MyModule
import qualified MyOtherModule

someFunction text = 'c' : MyModule.remove_e text -- Will work, removes lower case e's
someOtherFunction text = 'c' : MyOtherModule.remove_e text -- Will work, removes all e's
someIllegalFunction text = 'c' : remove_e text -- Won't work as there is no remove_e defined

在后面的代码片段中,没有名为remove_e的函数可用。当我们进行限定导入时,所有导入的值都包含模块名称作为前缀。顺便说一下,你也可以使用相同的这些前缀,即使你做了常规导入(在我们的例子中,MyModule.remove_e即使不包含 "qualified" 关键字也能正常工作)。

注意

限定名称(如 MyModule.remove_e)和函数组合运算符 (.) 之间存在歧义。写 reverse.MyModule.remove_e 很可能会让你的 Haskell 编译器感到困惑。一个解决方案是风格:始终使用空格进行函数组合,例如 reverse . remove_eJust . remove_e 甚至 Just . MyModule.remove_e


隐藏定义

[编辑 | 编辑源代码]

现在假设我们想导入两个模块MyModuleMyOtherModule,但我们确定要删除所有 e,而不仅仅是小写 e。添加MyOtherModule在每次调用remove_e之前将变得非常繁琐。我们不能仅仅排除remove_eMyModule?

import MyModule hiding (remove_e)
import MyOtherModule

someFunction text = 'c' : remove_e text

?这行得通,因为导入行上的 **hiding** 关键字。在 "hiding" 关键字后面的任何内容都不会被导入。通过使用括号和逗号分隔列出它们,隐藏多个项目

import MyModule hiding (remove_e, remove_f)

请注意,代数数据类型和类型别名无法隐藏。它们总是被导入。如果你在多个导入的模块中定义了数据类型,则必须使用限定名称。

重命名导入

[编辑 | 编辑源代码]

这实际上不是一种允许覆盖的技术,但它通常与 qualified 标志一起使用。想象一下

import qualified MyModuleWithAVeryLongModuleName

someFunction text = 'c' : MyModuleWithAVeryLongModuleName.remove_e text

尤其是在使用 qualified 时,这会变得令人厌烦。我们可以使用 **as** 关键字改进它

import qualified MyModuleWithAVeryLongModuleName as Shorty

someFunction text = 'c' : Shorty.remove_e text

这允许我们使用Shorty而不是MyModuleWithAVeryLongModuleName作为导入函数的前缀。这种重命名对限定导入和常规导入都有效。

只要没有冲突的项目,我们就可以导入多个模块并将它们重命名为相同

import MyModule as My
import MyCompletelyDifferentModule as My

在这种情况下,中的函数MyModule和中的函数MyCompletelyDifferentModule都可以以 My 为前缀。

将重命名与限制导入相结合

[编辑 | 编辑源代码]

有时用相同的模块对导入指令使用两次会很方便。一个典型的场景如下

import qualified Data.Set as Set
import Data.Set (Set, empty, insert)

这通过别名 "Set" 提供了对 Data.Set 模块的所有访问,并且还允许你访问一些选定的函数(empty、insert 和构造函数),而无需使用 "Set" 前缀。

在本文开头的示例中,使用了 "导入 *从 MyModule 导出* 的所有内容" 的说法。[3] 这提出了一个问题。我们如何决定哪些函数被导出,哪些函数保持 "内部"?方法如下

module MyModule (remove_e, add_two) where

add_one blah = blah + 1

remove_e text = filter (/= 'e') text

add_two blah = add_one . add_one $ blah

在这种情况下,只有remove_eadd_two被导出。虽然add_two被允许使用add_one,但导入MyModule的模块中的函数不能直接使用add_one,因为它没有被导出。

数据类型导出规范的写法类似于导入。你命名类型,并在括号中列出构造函数

module MyModule2 (Tree(Branch, Leaf)) where

data Tree a = Branch {left, right :: Tree a} 
            | Leaf a

在这种情况下,模块声明可以改写为 "MyModule2 (Tree(..))",声明所有构造函数都被导出。

维护导出列表是一个良好的习惯,不仅因为它减少了命名空间污染,而且因为它使某些 编译时优化 成为可能,否则这些优化将不可用。

笔记

  1. 有关模块系统的更多详细信息,请参见 Haskell 报告
  2. 在 Haskell98 中,Haskell 2010 之前 Haskell 的最后一个标准化版本,模块系统相当保守,但最近的常见做法是使用分层模块系统,使用句号划分命名空间。
  3. 一个模块可以导出它导入的函数。相互递归模块是可能的,但需要 一些特殊处理
华夏公益教科书