Skip to content

[译] Haskell 构建工具教程

B1nj0y edited this page Nov 10, 2018 · 6 revisions

原文 original / License

作者:Dmitrii Kovanikov

翻译:B1nj0y

Haskell: Build Tools

该文描述了两种管理和构建 Haskell 项目的主要方式,分别是使用 cabalstack 构建工具。该文无意取代这两者任一之官方文档,亦非想囊括涵盖所有构建 Haskell 项目的方式方法。而是意图竭尽所能地给予初学者在如何创建以及构建一个简单的项目时以明确的操作步骤。目标是消除其迷思,尤以初涉 Haskell 者为要。虽然官方文档对于获取深层次的理解最有裨益,但偶尔你也需要一些更直给的内容把事情搞定。

简介

Haskell 是一门编译型语言,而同时也有自己的解释器。使用解释器你可以对 Hackage (Haskell 集中包管理仓库) 中的一些包进行实验。这既可以在一个小的单个文件中进行,也可以在一个叫做 ghci 的交互式的 REPL 环境下操作。但是对于一些大项目而言,最好还是把源代码按照模块的方式来进行组织,这样更有利于维护。像 cabalstack 这样的构建工具就是帮助你管理该构建过程的。

Hackage 上有着大量的库,你并不一定会顺利地就找到所需要的那个合适的函数。幸运的是你可以使用 hoogle 通过名称或类型进行搜索。即便如此,Haskell 又有多个构建工具,而选择用其中哪一个也是一个头痛的问题。本文会重点讲述这其中的两个工具:

  1. cabal-install
  2. stack

之所以选择这俩是因为它们最为流行的同时也很成熟。我不会冒然说其中哪一个更好,而是会告诉你两者在包开发过程中的整体思路是怎样的。很希望在阅读完本文后你能自己选择更适合你的那一个。

Cabal

cabal 这个词在 Haskell 中算是在语意上进行重载了。在 Haskell 中,cabal 可以指:

  1. cabal-install
    • Haskell 项目的构建工具
  2. .cabal 为后缀的文件格式
    • 比如 foo 这个包其配置信息(模块、依赖、元信息等) 必须被写入一个叫做 foo.cabal 的文件,且使用符合 cabal-install 命令可读的特定语法
  3. Haskell 的一个库 Cabal
    • 一个库, cabal-install 用其对 .cabal 格式文件实现了一个解析器

务请莫在没有上下文的情况下使用 "CABAL" 一词!

在本文中,当我没有明确指明时,cabal 则是指 cabal-install

安装

Ubuntu

最新版本的 cabalghc 都可以通过 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

在 macOS 上获取 cabal 命令的话还是使用 brew 就好:

$ brew install ghc cabal-install

如果你想在 macOS 上使用多个版本的 GHC,你会发现这个精炼的 haskell-on-macos.py 脚本很好用:

Windows

对于 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

你只能在如下情形之一时执行此命令:

  1. 尚未运行过此命令;
  2. 你删除过 ~/.cabal 这个文件夹;
  3. 在你上次执行过 cabal new-update 命令后,你想使用其中一个库的更新一些的版本;

提示: 当构建依赖时出现了一些莫名其妙的错误(比如那些骇人的 linker 错误), 这时删除 ~/.cabal 文件夹可能会有效。

最后到了真正构建这个包的这一步了,执行命令:

$ cabal new-build

重要提示: cabal 还有两个不带 new- 这个前缀的命令:buildupdate。千万莫用这两个命令!要用带 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 或许不能完成项目的构建,因其无从知晓该为这些包选哪一个版本为好。

REPL

好啦,我们写了两个函数。然后要不要测试一下?前面提到过 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

即便你仅对如何使用 stack 更感兴趣,你还是应该先读下上面 cabal 章节的内容,因为我会在接下来的一些部分里用到它。

安装

stack 官方文档的开头你可以找到如何构建 stack 的操作指南:

你并不需要单独安装 ghc,因为 stack 会给你下载一个配套的编译器。

Unix

在任何 unix 系统上安装 stack 都想当容易:

$ curl -sSL https://get.haskellstack.org/ | sh

Windows

对于 Windows 用户,可以从其网站直接下载官方的二进制版本:

初始化项目

创建一个名为 stack-example 的新项目,你可以执行:

$ stack new stack-example

提示: 为了能使用 stack 成功完成项目的初始化,你的机器需要联入互联网。因为目前还没有实现离线的版本。

cabal init 命令不同的是,stack new 并不会问你任何问题。它仅仅会按照一个默认的模版创建一个项目。然而你可以为其指定一个已有的模版(你其实也可以创建一个自己的模版):

你也可以选择使用 summoner 工具创建项目。所有的 stack 模版都描述了一个固定的项目结构,同时提供的可配置元数据也仅有诸如 user name 这样简单的信息。但是有时你可能想要的更多,比如可以在包里添加 benchmarks 这样的节配置(stanza)。通常来说你并不想要一堆仅对某个主模版做了轻微修改的多个模版。 summoner 可以自动为你的新项目创建一个 GitHub 仓库,所以它是介于 cabal initstack 模版两者之间一种做法,同时也还有一些其它的特性。

项目结构

默认 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, executabletest-suite 等节配置被添加进 .cabal 文件中,同时与之相对应的 Haskell 代码会指定放入 src/, app/test/ 目录。

stack.yaml 文件包含一些相对于 stack 而言额外的配置项。

package.yaml 这个配置文件则包含了另一种格式对包的描述,该格式被用于 hpack(长文慎入)。这样你则使用 YAML 替代 cabal 的格式来写包的描述文档,由此你还会获取到一些额外的特性,比如模块自动发现等。然后 hpack 会使用 package.yaml 中的信息生成一个 .cabal 的文档。但是如果你暂时还不想摆弄 hpack 的话你直接把 package.yaml 文件删了就行了。

构建项目

使用如下命令可以构建包含 library, executabletest-suite 节配置的项目,同时还可以运行测试。

$ stack build --test

stack 在第一次运行构建时会自动下载正确的 GHC 版本。

LTS resolver

stackcabal 最主要的区别在于,这两者如何确定正确的包依赖的版本。它们都使用 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 一栏中添加该库及其版本号。 查看示例

REPL 及可执行文件

可以使用 stack repl 命令在项目内启动 ghci 命令行。

使用如下命令运行一个指定节配置(stanza)的可执行文件:

$ stack exec name-of-my-executable

结语

总结的话,我想说的是 cabalstack 的工作流看起来的确很类似,但是其背后的思想却大不一样。我建议读者对两者都做一番尝试之后再决定用哪一个会更好。希望上面讲的这一波操作会对你开始创建 Haskell 项目有所帮助!

补充

从构建工具这个角度说,对 cabal 以及 stack 的有益补充应该是 nixNix 虽然也是一个很流行的选择,但对初学者不够友好。如果你有志于学习更多如何将 nix 应用到 Haskell 的知识,我建议你阅读下面的教程:

如果你对更多构建 Haskell 包的实验性方式有兴趣,可以查看下面这些项目:

译者补充

译者根据本文建立的项目示例

tags: haskell, stack, cabal, build-tools, tutorial