-
Notifications
You must be signed in to change notification settings - Fork 2
[译] Haskell 构建工具教程
翻译:B1nj0y
该文描述了两种管理和构建 Haskell 项目的主要方式,分别是使用 cabal
和 stack
构建工具。该文无意取代这两者任一之官方文档,亦非想囊括涵盖所有构建 Haskell 项目的方式方法。而是意图竭尽所能地给予初学者在如何创建以及构建一个简单的项目时以明确的操作步骤。目标是消除其迷思,尤以初涉 Haskell 者为要。虽然官方文档对于获取深层次的理解最有裨益,但偶尔你也需要一些更直给的内容把事情搞定。
Haskell 是一门编译型语言,而同时也有自己的解释器。使用解释器你可以对 Hackage
(Haskell 集中包管理仓库) 中的一些包进行实验。这既可以在一个小的单个文件中进行,也可以在一个叫做 ghci
的交互式的 REPL 环境下操作。但是对于一些大项目而言,最好还是把源代码按照模块的方式来进行组织,这样更有利于维护。像 cabal
和 stack
这样的构建工具就是帮助你管理该构建过程的。
Hackage 上有着大量的库,你并不一定会顺利地就找到所需要的那个合适的函数。幸运的是你可以使用 hoogle 通过名称或类型进行搜索。即便如此,Haskell 又有多个构建工具,而选择用其中哪一个也是一个头痛的问题。本文会重点讲述这其中的两个工具:
-
cabal-install
-
stack
之所以选择这俩是因为它们最为流行的同时也很成熟。我不会冒然说其中哪一个更好,而是会告诉你两者在包开发过程中的整体思路是怎样的。很希望在阅读完本文后你能自己选择更适合你的那一个。
cabal
这个词在 Haskell 中算是在语意上进行重载了。在 Haskell 中,cabal
可以指:
-
cabal-install
- Haskell 项目的构建工具
- 以
.cabal
为后缀的文件格式- 比如
foo
这个包其配置信息(模块、依赖、元信息等) 必须被写入一个叫做foo.cabal
的文件,且使用符合cabal-install
命令可读的特定语法
- 比如
- Haskell 的一个库
Cabal
- 一个库,
cabal-install
用其对.cabal
格式文件实现了一个解析器
- 一个库,
务请莫在没有上下文的情况下使用 "CABAL" 一词!
在本文中,当我没有明确指明时,cabal
则是指 cabal-install
。
最新版本的 cabal
和 ghc
都可以通过 hvr/ghc
PPA 来安装:
使用如下命令进行安装:
$ sudo add-apt-repository ppa:hvr/ghc
$ sudo apt update
$ sudo apt install ghc-8.4.3 cabal-install-head
提示: 最好还是要安装
cabal
HEAD 的版本,因其对我们接下来将用到的使用cabal
来构建项目的命令有更好的支持。
你可以如下这般来检查一番安装好的这些预编译二进制的版本:
$ /opt/ghc/bin/ghc --version
The Glorious Glasgow Haskell Compilation System, version 8.4.3
$ /opt/ghc/bin/cabal --version
cabal-install version 2.3.0.0
compiled using version 2.3.0.0 of the Cabal library
为了使开发时操作起来更方便,你还是要把这些二进制可执行文件加入到当前 $PATH
环境变量里:
$ echo 'export PATH="$PATH:/opt/ghc/bin"' >> ~/.profile
$ . ~/.profile
在 macOS 上获取 cabal
命令的话还是使用 brew
就好:
$ brew install ghc cabal-install
如果你想在 macOS 上使用多个版本的 GHC,你会发现这个精炼的 haskell-on-macos.py
脚本很好用:
对于 Windows 的话安装 cabal
以及 ghc
最简便的方式是安装 Haskell Platform,你可以按照如下链接中的指南进行安装:
另外你也可以使用 chocolatey
, 一个针对 Windows 的包管理器:
通过运行 cabal init
命令来初始化一个新的项目。Cabal 将通过一个交互式过程来指引你操作,同时会问你几个有关项目目录结构方面的问题。
$ mkdir cabal-example # 创建项目目录
$ cd cabal-example # 进入到该文件夹
$ cabal init # 在该目录下初始化项目
作为替代方案,你也可以选择 summoner
这个工具。summoner
会为你生成比 cabal init
命令更多的内容,而且还不会问你一堆啰嗦的问题。或许你的第一个 Haskell 项目还是用 cabal init
比较好,因为 summoner
毕竟要先安装好才能用的。
比如说你准备创建 cabal-example
这个项目,选择 src
作为源代码的目录。
在你使用 cabal-example
作为名字,选择构建一个 Library
,同时把 src/
作为源代码目录来初始化一个包项目之后,你会得到如下的目录结构:
$ tree cabal-example
cabal-example/
├── ChangeLog.md
├── LICENSE
├── Setup.hs
├── src/
└── cabal-example.cabal
1 directory, 4 files
cabal-example.cabal
描述了你这个包项目的结构。你要把你的源码放到 src/
这个文件夹里。不必关心那个 Setup.hs
文件,它大多时候基本没啥用,可以完全无视或是直接删了就完了。
要构建这个项目,你得有几行代码才行。我们就在 src/
下面创建一个 Dummy.hs
文件,写入如下内容:
module Dummy where
inc :: Int -> Int
inc x = x + 1
然后把 cabal-example.cabal
中的这一行:
-- exposed-modules:
替换为:
exposed-modules: Dummy
这是告诉 cabal
该模块现在是你这个包的一部分。
然后你需要更新一下 Hackage 的索引,这样的话 cabal
会获取到 Haskell 所有包的最新版本。为此你需要在项目根目录运行如下命令:
$ cabal new-update
你只能在如下情形之一时执行此命令:
- 尚未运行过此命令;
- 你删除过
~/.cabal
这个文件夹; - 在你上次执行过
cabal new-update
命令后,你想使用其中一个库的更新一些的版本;
提示: 当构建依赖时出现了一些莫名其妙的错误(比如那些骇人的 linker 错误), 这时删除
~/.cabal
文件夹可能会有效。
最后到了真正构建这个包的这一步了,执行命令:
$ cabal new-build
重要提示:
cabal
还有两个不带new-
这个前缀的命令:build
和update
。千万莫用这两个命令!要用带new-
前缀的:它们工作的更好些。实现一个合适的构建工具的确是一个艰难的任务,不是一下子就能很清晰地认识到如何把事情搞定。但这也不是说一旦你又找到一个更好的方式时依然把它实现在旧的接口上。包的作者们要为其用户多考虑:使用老套路构建包的人们还是可以继续使用,同时也用new-
前缀的新命令提供了更正确的方式。在cabal-3.0
版本中会用新的命令替换掉老的命令,同时这些前缀也就被移除了。
所有的依赖都应该在 .cabal
配置文件的 build-depends
条目下指定。每一个新的包都默认把 base
库作为一个依赖。但是在 Hackage 上可是有大量可用的包!举个例子说,要处理随机数的话我们可以用 random
这个包。为此你需要在 .cabal
文件中如下这般修改 build-depends
这一栏:
build-depends: base >=4.11 && <4.12, random
然后我们就可以来用该包中的任何函数啦!
module Dummy where
import System.Random (randomRIO)
inc :: Int -> Int
inc x = x + 1
dice :: IO Int
dice = randomRIO (1, 6)
之后执行 cabal new-build
以确保所有的内容再次完成构建。
提示: 一个包通常都是以一个相同的名称但多个版本的形式存在。你会发现在你的
.cabal
文件里会为base
这个包指定其版本界限。如果你有大量的包依赖没有明确给出版本限定,cabal
或许不能完成项目的构建,因其无从知晓该为这些包选哪一个版本为好。
好啦,我们写了两个函数。然后要不要测试一下?前面提到过 ghc
有一个交互式的编译器。你可以通过如下的命令启动它:
$ cabal new-repl
然后在 REPL 中运行我们刚写的代码:
ghci> inc 64
65
ghci> dice
2
ghci> dice
1
ghci> :q
Leavning GHCi.
$
可能每次执行 dice
函数并不会看到其输出相同的数字,但你还是有 1/36
的几率会看到它会输出相同的数字。
在 REPL 下执行函数可谓其乐无穷啊!但程序通常都是实现成可执行文件的形式以做后用。前面也提到了 Haskell 是可编译语言,所以你可以生成原生的二进制文件。为此你需要在 .cabal
文件中添加 executable
配置。
提示: 在 cabal 格式规范中,诸如
library
,executable
及其它的这些顶级区域部分的配置被称作 节(stanza)。
我们通过在 .cabal
配置文件的结尾处添加如下的几行来示范一下如何配置一个可执行文件的这一节(stanza)。
executable my-exe
main-is: Main.hs
build-depends: base, cabal-example
default-language: Haskell2010
提示: 我们把
cabal-example
添加到build-depends
,这样就可以使用来自library
一节中的函数了。此处不必为base
添加版本限定,因其可以从cabal-example
中获取到。
接下来我们需要在项目根目录创建 Main.hs
文件。该文件除了必须包含一个 main :: IO ()
的函数外,其余的你想写啥都如君所愿的往里面写就是。发挥你的想象力吧!我反正就随便写了这么几行:
module Main where
import Dummy (dice)
main :: IO ()
main = do
putStrLn "777"
n <- dice
print n
我们给可执行文件起了个 my-exe
的名字,所以可以通过执行 cabal new-exec
命令来启动它:
$ cabal new-build # 在对项目做了修改后不要忘了重新执行构建
$ cabal new-exec my-exe
恭喜你,你刚刚运行了你的首个 Haskell 程序!
推荐一个更详细同时也是对初学者友好的 cabal
入门播客:
即便你仅对如何使用 stack
更感兴趣,你还是应该先读下上面 cabal
章节的内容,因为我会在接下来的一些部分里用到它。
在 stack
官方文档的开头你可以找到如何构建 stack
的操作指南:
你并不需要单独安装 ghc
,因为 stack
会给你下载一个配套的编译器。
在任何 unix 系统上安装 stack
都想当容易:
$ curl -sSL https://get.haskellstack.org/ | sh
对于 Windows 用户,可以从其网站直接下载官方的二进制版本:
创建一个名为 stack-example
的新项目,你可以执行:
$ stack new stack-example
提示: 为了能使用
stack
成功完成项目的初始化,你的机器需要联入互联网。因为目前还没有实现离线的版本。
与 cabal init
命令不同的是,stack new
并不会问你任何问题。它仅仅会按照一个默认的模版创建一个项目。然而你可以为其指定一个已有的模版(你其实也可以创建一个自己的模版):
你也可以选择使用 summoner
工具创建项目。所有的 stack
模版都描述了一个固定的项目结构,同时提供的可配置元数据也仅有诸如 user name 这样简单的信息。但是有时你可能想要的更多,比如可以在包里添加 benchmarks
这样的节配置(stanza)。通常来说你并不想要一堆仅对某个主模版做了轻微修改的多个模版。 summoner
可以自动为你的新项目创建一个 GitHub 仓库,所以它是介于 cabal init
和 stack
模版两者之间一种做法,同时也还有一些其它的特性。
默认 stack
模版生成的项目目录结构大致如下:
$ tree stack-example/
stack-example/
├── app/
│ └── Main.hs
├── ChangeLog.md
├── LICENSE
├── package.yaml
├── README.md
├── Setup.hs
├── src/
│ └── Lib.hs
├── stack-example.cabal
├── stack.yaml
└── test/
└── Spec.hs
3 directories, 10 files
在此你发现 stack
会生成比 cabal
命令多得多的文件。library
, executable
及 test-suite
等节配置被添加进 .cabal
文件中,同时与之相对应的 Haskell 代码会指定放入 src/
, app/
和 test/
目录。
stack.yaml
文件包含一些相对于 stack
而言额外的配置项。
package.yaml
这个配置文件则包含了另一种格式对包的描述,该格式被用于 hpack
(长文慎入)。这样你则使用 YAML 替代 cabal
的格式来写包的描述文档,由此你还会获取到一些额外的特性,比如模块自动发现等。然后 hpack
会使用 package.yaml
中的信息生成一个 .cabal
的文档。但是如果你暂时还不想摆弄 hpack
的话你直接把 package.yaml
文件删了就行了。
使用如下命令可以构建包含 library
, executable
和 test-suite
节配置的项目,同时还可以运行测试。
$ stack build --test
stack
在第一次运行构建时会自动下载正确的 GHC 版本。
stack
和 cabal
最主要的区别在于,这两者如何确定正确的包依赖的版本。它们都使用 Cabal
库解析 .cabal
文件。然而 stack
使用了 LTS resolver 这样的一个概念。简而言之,resolver 就是一个 Hackage 快照,这个快照里的包之间可以愉快地共处。翻看一下 stack.yaml
配置文件,会看到大约这样的一行:
resolver: lts-11.14
提示: 仅此一行是
stack.yaml
对于stack
在大多情形下能正常工作所必需的一行。默认的stack.yaml
配置会包含大量多余的内容。除了这一行你可以大胆地删除其余所有的内容,除非你需要其它更高级的一些用法。
stack
添加依赖几乎和 cabal
一模一样:只需向 build-depends
一栏添加你需要的库就可以了。
如果你对于 build-depends
中加入的包在你的项目里该使用哪个版本有所疑惑,可以打开 Stackage 网站,通过你在 stack.yaml
中指定的 resolver 来搜索你所需的库。举例说,本文中我们需要打开:
你也可以通过终端达成这个操作。比如,对于 random
库的话我们就运行:
$ stack ls dependencies | grep random
有时有的库可能在指定的 resolver 中并不存在。这样的话你需要在 stack.yaml
文件的 extra-deps
一栏中添加该库及其版本号。
查看示例
可以使用 stack repl
命令在项目内启动 ghci
命令行。
使用如下命令运行一个指定节配置(stanza)的可执行文件:
$ stack exec name-of-my-executable
总结的话,我想说的是 cabal
和 stack
的工作流看起来的确很类似,但是其背后的思想却大不一样。我建议读者对两者都做一番尝试之后再决定用哪一个会更好。希望上面讲的这一波操作会对你开始创建 Haskell 项目有所帮助!
从构建工具这个角度说,对 cabal
以及 stack
的有益补充应该是 nix
。Nix 虽然也是一个很流行的选择,但对初学者不够友好。如果你有志于学习更多如何将 nix
应用到 Haskell 的知识,我建议你阅读下面的教程:
如果你对更多构建 Haskell 包的实验性方式有兴趣,可以查看下面这些项目:
译者根据本文建立的项目示例。
tags: haskell, stack, cabal, build-tools, tutorial