# 模块、包和项目 ## 使用模块组织代码 每一个Haskell应用都是一组模块的集合,每一个模块都可以导入来自其他模块、基础库以及其他库的定义;除此之外,模块还可以到处一些定义,这些定义对于其他模块将变得可见(可以被其他模块导入)。在众多模块中有一个特殊的模块`Main`,该模块作为程序进入点,有且仅有一个。 ### 模块介绍 一个Haskell模块的结构如下: ```hs module ModuleName () where -- set of imports -- declarations for main codes such as -- + data types and type synonyms -- + type classes and instances -- + functions -- etc ``` 其中模块名称`ModuleName`首字母必须大写,且建议与文件名称一致。 对于导入模块: - `import SomeModule` 可以导入整个模块 - `import SomeModule(name1,name2,...)` 可以导入模块中指定的名字 - `import SomeModule()` 不导入模块的任何名字,不具有名字的实例除外 - `import qualified SomeModule`导入整个模块但保留完整前缀,`import qualified SomeModule(name1,name2,...)`导入指定名字但保留完成前缀;这意味我们必须使用`SomeModule.name1`对`name1`进行调用 - `import qualified SomeModule as Alias`导入整个模块并使用别名`Alias`;这意味我们必须使用`Alias.name1`对`name1`调用,而非`SomeModule.name1` - `import SomeModule hiding (name1,name2,...)` 导入整个模块除了`name1`、`name2`等 对于导出列表``来说: - 可以导出函数的名称 - 可以导出数据类型(使用`(..)`跟随类型构造器,可包含值(全部或部分)构造器) - 类型类的名称(使用`(..)`跟随类型类名称,可包含(全部或部分)内部方法) - 已经导入的模块的名称或同义词 例如: ```hs module ModuleName ( module X, module Y, DataType1, DataType2 (..), DataType3 (Cons1, Cons2), TypeClass1, TypeClass2 (..), TypeClass3 (method1,method2), fun1, fun2, fun3 ) where import X import qualified SomeModule as Y ``` 使用以上的导出,当我们在其他模块中导入该模块时,可以使用的是: - 模块X和模块Y中导出的任何内容 - `DataType1` 类型构造器,但我们无法进行模式匹配或构造该类型的值 - `DataType2` 类型构造器以及内部所有的值构造器 - `DataType3` 类型构造器以及内部的两个构造器`Con1`和`Con2`,如果另有构造器`Con3`则该构造器无法被使用 - `TypeClass1` 类型类,内部方法无法使用 - `TypeClass2` 类型类,内部方法只有`method1`和`method2`可以被使用 - `TypeClass3`类型类以及全部方法均可以被使用 - `fun1`、`fun2`、`fun3`三个函数 > 补充:``并不是必须的,我们可以声明`module ModuleName where`以便模块可以全部导出;但通常情况下,除非模块内容非常少,否则不建议这样做 ### 模块层次 模块名称具有层次的,可以使用`.`对元素进行分隔,如`Data.Typeable`。一般地,模块层次及其名称有如下对应关系:
![](/_static/image/module.png)
## 包 一个 ***包(package)*** 可以认为由两部分组成: 1. 对每个导出模块的接口文件 2 编译过的(静态或者动态)库文件 当编译器编译程序时,首先读取源文件导入的模块名称,接着在包数据库中查找哪些包提供了需要的模块以及对应的构建在文件系统的位置;然后编译器查找这些模块的接口文件并使用文件中的信息编译模块,在链接阶段,会查找由这些包提供的静态或者动态库;最后编译器链接所有的静态或动态库得到可执行文件或库。 `ghc-pkg`命令用于查找数据库中包的信息,我们可以使用`ghc-pkg list`查看当前注册的所有包的名称和版本;`ghc-pkg describe ...`命令可以进一步查看某个包更详细的信息;或者,我们还可以通过命令`ghc-pkg find-module ...`查找某个模块所对应的包。 ```bash Prelude> ghc-pkg list /home/user/.ghcup/ghc/8.6.4/lib/ghc-8.6.4/package.conf.d Cabal-2.4.0.1 array-0.5.3.0 base-4.12.0.0 ... Prelude> ghc-pkg describe base name: base version: 4.12.0.0 id: base-4.12.0.0 key: base-4.12.0.0 license: BSD-3-Clause maintainer: libraries@haskell.org ... Prelude> ghc-pkg find-module Data.Typeable /home/user/.ghcup/ghc/8.6.4/lib/ghc-8.6.4/package.conf.d base-4.12.0.0 ``` > 提示: `ghc-pkg`命令查找的是GHC的全局数据库,除了将包安装到全局,我们还可以使用项目管理工具,为某个项目设置单独的环境,将需要的包安装到相应的项目上。 ### 包管理工具Cabal Cabal库定义了Haskell包的信息并提供了操作包的工具(包括但不限于获取包的信息、构建包、注册包)。 Cabal包中有两个至关重要的文件,其中之一以`.cabal`后缀结尾,该文件定义了包内详细的描述信息;另外一个为`Setup.hs`文件,其为但文件程序,用于执行各种设置任务,该文件内容由`.cabal`文件中的生成类型设置确定,一般情况下我们只需交给Cabal生成该文件即可。 对于一个`.cabal`文件来说,其包含: - 被其他包调用模块的库组件 - 任意多个可执行组件 - ***测试套件(test suites)*** - ***基准测试套件(benchmark suites)*** - 由主程序库,可执行组件库,测试套件和基准测试套件共享的内部库 下面给出[官网](https://cabal.readthedocs.io/en/latest/cabal-package-description-file.html)几个示例: - 包含简单库的包(HUnit package) ``` cabal-version: 3.0 name: HUnit version: 1.1.1 synopsis: A unit testing framework for Haskell homepage: http://hunit.sourceforge.net/ category: Testing author: Dean Herington license: BSD-3-Clause license-file: LICENSE build-type: Simple library build-depends: base >= 2 && < 4 exposed-modules: Test.HUnit.Base, Test.HUnit.Lang, Test.HUnit.Terminal, Test.HUnit.Text, Test.HUnit default-extensions: CPP default-language: Haskell2010 ``` - 包含可执行程序的包 ``` cabal-version: 3.0 name: TestPackage version: 0.0 synopsis: Small package with two programs author: Angela Author license: BSD-3-Clause build-type: Simple executable program1 build-depends: HUnit >= 1.1.1 && < 1.2 main-is: main.hs hs-source-dirs: prog1 default-language: Haskell2010 executable program2 -- A different main.hs because of hs-source-dirs. main-is: main.hs build-depends: HUnit >= 1.1.1 && < 1.2 hs-source-dirs: prog2 other-modules: Utils default-language: Haskell2010 ``` - 包含库和可执行程序 ``` cabal-version: 3.0 name: TestPackage version: 0.0 synopsis: Package with library and two programs license: BSD-3-Clause author: Angela Author build-type: Simple library build-depends: HUnit >= 1.1.1 && < 1.2 hs-source-dirs: lib exposed-modules: A, B, C default-language: Haskell2010 executable program1 main-is: main.hs hs-source-dirs: prog1 other-modules: D, E default-language: Haskell2010 executable program2 -- A different main.hs because of hs-source-dirs. main-is: main.hs -- No bound on a library provided by the same package. build-depends: TestPackage hs-source-dirs: prog2 other-modules: Utils default-language: Haskell2010 ``` - 包含测试套件 ``` Cabal-Version: 3.0 Name: bar Version: 1.0 License: BSD-3-Clause Build-Type: Simple Test-Suite test-bar type: detailed-0.9 test-module: Bar build-depends: base >= 4 && < 5, Cabal >= 1.9.2 && < 2 default-language: Haskell2010 ``` - 包含基准测试套件 ``` Cabal-Version: 3.0 Name: foo Version: 1.0 License: BSD-3-Clause Build-Type: Simple Benchmark bench-foo type: exitcode-stdio-1.0 main-is: bench-foo.hs build-depends: base >= 4 && < 5, time >= 1.1 && < 1.7 default-language: Haskell2010 ``` 有关参数的细致说明,读者可以移步[官网](https://cabal.readthedocs.io/en/latest/cabal-package-description-file.html)按需查询,限于篇幅这里不再赘述。 ## 从零创建一个简单的项目 > 提示: 不同的Cabal版本构建项目过程不尽相同,但原理基本一致 本节将提供一个简单且完整的可执行项目示例,该项目功能未读取用户输入的数字,并输出对应的阶乘结果。 首先,使用`Cabal`管理工具初始化项目`cabal init`,这里为了便于读者了解创建过程,使用交互模式初始化`cabal init --interactive`。 ```bash $ mkdir myproject $ cd myproject $ cabal init --interactive Should I generate a simple project with sensible defaults? [default: y] n What does the package build: 1) Executable 2) Library 3) Library and Executable Your choice? 1 What is the main module of the executable: * 1) Main.hs (does not yet exist, but will be created) 2) Main.lhs (does not yet exist, but will be created) 3) Other (specify) Your choice? [default: Main.hs (does not yet exist, but will be created)] Please choose version of the Cabal specification to use: 1) 1.10 (legacy) 2) 2.0 (+ support for Backpack, internal sub-libs, '^>=' operator) 3) 2.2 (+ support for 'common', 'elif', redundant commas, SPDX) * 4) 2.4 (+ support for '**' globbing) 5) 3.0 (+ set notation for ==, common stanzas in ifs, more redundant commas, better pkgconfig-depends) Your choice? [default: 2.4 (+ support for '**' globbing)] Package name? [default: myproject] Package version? [default: 0.1.0.0] Please choose a license: * 1) NONE 2) BSD-2-Clause 3) BSD-3-Clause 4) Apache-2.0 5) MIT 6) MPL-2.0 7) ISC 8) GPL-2.0-only 9) GPL-3.0-only 10) LGPL-2.1-only 11) LGPL-3.0-only 12) AGPL-3.0-only 13) GPL-2.0-or-later 14) GPL-3.0-or-later 15) LGPL-2.1-or-later 16) LGPL-3.0-or-later 17) AGPL-3.0-or-later 18) Other (specify) Your choice? [default: NONE] Author name? [default: waste-of-kindergarten] Maintainer email? [default: 2020141461068@stu.scu.edu.cn] Project homepage URL? Project synopsis? Project category: * 1) (none) 2) Codec 3) Concurrency 4) Control 5) Data 6) Database 7) Development 8) Distribution 9) Game 10) Graphics 11) Language 12) Math 13) Network 14) Sound 15) System 16) Testing 17) Text 18) Web 19) Other (specify) Your choice? [default: (none)] Application (Main.hs) directory: * 1) app 2) src-exe 3) (none) 4) Other (specify) Your choice? [default: app] What base language is the package written in: * 1) Haskell2010 2) Haskell98 3) Other (specify) Your choice? [default: Haskell2010] Add informative comments to each field in the cabal file (y/n)? [default: n] Guessing dependencies... Generating CHANGELOG.md... Generating app/Main.hs... Generating myproject.cabal... Warning: no synopsis given. You should edit the .cabal file and add one. You may want to edit the .cabal file and add a Description field. ``` 完成创建后,我们可以看到`myproject`文件夹中多了如下内容: ```bash $ tree -- sudo apt install tree . ├── app │ └── Main.hs ├── CHANGELOG.md └── myproject.cabal 1 directory, 3 files ``` 项目文件夹比较简单,只有`app`文件夹,`.cabal`文件以及一个说明文件`CHANGELOG.md`。 我们首先在`app`文件夹中创建一个名为`Fac.hs`的文件,并在内部添加阶乘函数: ```hs -- Fac.hs module Fac (factorial) where factorial :: Integer -> Integer factorial x = if x < 0 then error " x < 0 not allowed" else fun x fun :: Integer -> Integer fun 0 = 1 fun x = x * fun (x - 1) ``` 这里为了避免频繁地进行条件判断,在`factorial`函数中只进行一次范围判断,对于合法的数字,定义`fun`函数用于正常的阶乘运算。 然后修改`Main.hs`文件如下: ```hs module Main where import Fac import System.Environment(getArgs) main :: IO () main = do args <- getArgs let num = read (head args) :: Integer print $ factorial num ``` 主程序从命令行中读取用户输入的数字参数,并将其转化为整数,最终输出阶乘的结果。 最后,为了让主程序能够探测到`Fac`模块的位置,需要修改`.cabal`文件,将`other-modules`取消注释并添加`Fac`。 ``` ... executable myproject main-is: Main.hs -- Modules included in this executable, other than Main. other-modules: Fac ... ``` 保存完毕后,编译文件并执行。 ```bash # 编译 $ cabal build Build profile: -w ghc-8.6.4 -O1 In order, the following will be built (use -v for more details): - myproject-0.1.0.0 (exe:myproject) (first run) Preprocessing executable 'myproject' for myproject-0.1.0.0.. Building executable 'myproject' for myproject-0.1.0.0.. [2 of 2] Compiling Main ( app/Main.hs, /home/user/myproject/dist-newstyle/build/x86_64-linux/ghc-8.6.4/myproject-0.1.0.0/x/myproject/build/myproject/myproject-tmp/Main.o ) Linking /home/user/myproject/dist-newstyle/build/x86_64-linux/ghc-8.6.4/myproject-0.1.0.0/x/myproject/build/myproject/myproject ... $ cabal run cabal run myproject -- "5" Up to date 120 ``` ## 项目的打包与安装 我们将上一节中的项目从可执行程序更改为库,重新初始化后文件层次如下: ```bash $ tree . ├── CHANGELOG.md ├── myproject.cabal ├── src │ └── MyLib.hs └── test └── MyLibTest.hs 2 directories, 4 files ``` 将`MyLib.hs`更改为`Fac.hs`,并将上一节的内容复制到这个文件中。 接着修改`.cabal`文件: ``` ... library exposed-modules: Fac ... ``` 我们希望在打包前能够测试一下这个包是否能够正常使用,因此还需要修改一下测试文件`MyLibTest.hs`: ```hs module Main (main) where import Fac import Test.HUnit import System.Exit tests :: Test tests = TestList [ "Test 1" ~: assertEqual "x > 0 ?" 120 (factorial 5), "Test 2" ~: assertEqual "x = 0 ?" 1 (factorial 0) ] main :: IO () main = do counts <- runTestTT tests if errors counts == 0 && failures counts == 0 then exitSuccess else exitFailure ``` 然后修改`.cabal`文件的测试部分: ``` ... test-suite myproject-test default-language: Haskell2010 type: exitcode-stdio-1.0 hs-source-dirs: test , src -- 此处添加src,以便Fac可见 main-is: MyLibTest.hs build-depends: base ^>=4.12.0.0, HUnit -- 添加额外的HUnit依赖 ``` 至此,我们就可以执行测试了: ```bash $ cabal test ... Running 1 test suites... Test suite myproject-test: RUNNING... Test suite myproject-test: PASS Test suite logged to: /home/user/myproject/dist-newstyle/build/x86_64-linux/ghc-8.6.4/myproject-0.1.0.0/t/myproject-test/test/myproject-0.1.0.0-myproject-test.log 1 of 1 test suites (1 of 1 test cases) passed. ``` 测试成功,接下来我们就可以将项目进行打包和安装。 ```bash $ cabal build & cabal run ... Linking /home/user/myproject/dist-newstyle/build/x86_64-linux/ghc-8.6.4/myproject-0.1.0.0/t/myproject-test/build/myproject-test/myproject-test ... Cases: 2 Tried: 2 Errors: 0 Failures: 0 # 创建一个归档 $ cabal sdist Wrote tarball sdist to /home/user/myproject/dist-newstyle/sdist/myproject-0.1.0.0.tar.gz ``` 对于本地项目我们可以直接`cabal install`,对于获得归档文件的用户来说,需要解压归档然后安装: ```bash $ tar -xf myproject-0.1.0.0.tar.gz $ cd myproject-0.1.0.0 $ cabal install --lib # 注意这个lib必须要加,否则会被认为安装的是和执行文件 Wrote tarball sdist to /home/user/myproject/myproject-0.1.0.0/dist-newstyle/sdist/myproject-0.1.0.0.tar.gz Resolving dependencies... Build profile: -w ghc-8.6.4 -O1 In order, the following will be built (use -v for more details): - myproject-0.1.0.0 (lib) (requires build) Starting myproject-0.1.0.0 (lib) Building myproject-0.1.0.0 (lib) Installing myproject-0.1.0.0 (lib) Completed myproject-0.1.0.0 (lib) ``` 至此,`myproject`包已经安装完成,现在可以尝试在GHCi中使用它了。 ```bash Prelude> import Fac Prelude> factorial 5 120 Prelude> factorial 0 1 Prelude> factorial (-1) *** Exception: x < 0 not allowed ```