跳转到内容

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 指令很方便。一个典型的场景如下

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

这使您可以通过别名“Set”访问 Data.Set 模块的所有内容,还可以让您访问几个选定的函数(empty、insert 和构造函数)而无需使用“Set”前缀。

在本文开头的示例中,使用了“import 所有导出的内容 from 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. 模块可以导出它导入的函数。相互递归模块是可能的,但需要一些特殊处理
华夏公益教科书