1. Haskell(一)入门
  2. Haskell(二)函数式编程
  3. Haskell(三) Monad
  4. Haskell(四)总结和工具链
  5. Haskell(五) 总结和展望
  6. Haskell(六) Project Euler 练习1-26

总结

工具链

stack

stack 非常常用的工具链,它有很多 snapshot,把一些特定版本的库,都集成在这个 snapshot 里,然后要编译时,就会自动拉取这些库。这样就提供了可以复现的环境。除此之外,还可以自定义依赖。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
resolver: lts-21.17

packages:
- '.'

extra-deps:
- git: https://github.com/ethereum/hevm.git
commit: 91d906b6593f2ba74748fff9a7d34eadf1980ceb

- restless-git-0.7@sha256:346a5775a586f07ecb291036a8d3016c3484ccdc188b574bcdec0a82c12db293,968
- s-cargot-0.1.4.0@sha256:61ea1833fbb4c80d93577144870e449d2007d311c34d74252850bb48aa8c31fb,3525

extra-include-dirs:
- /home/learner/.local/include
extra-lib-dirs:
- /home/learner/.local/lib

比如上面使用了 lts-21.17 的环境,所有的 snapshot 可以在 stackage 中查看。其他的是额外的依赖、额外的库,这里是 hevm 依赖了一些 C/CPP 写的密码学库。

stack path --stack-root 中就能查看到 stack 的存储位置,目录结构如下图所示。可以看到 config.yaml 是全局的配置,如果缺少项目的配置文件,就默认选择全局的,否则优先项目的配置。

在开发中,为了使用 stack 的环境,需要在命令前加 stack,比如说

1
2
stack ghc -- -O2 -o main
stack ghci

stack 会自动加载依赖,然后 -- 之后是传给 runghc 的参数,优化等级 2 然后编译成可执行文件 main。

另外 stack 可以自己安装一些可执行文件。

另外需要注意的是,在较新的版本中,我们一般不直接编辑stack.yam配置,而是有个 package.yaml 作为配置文件,然后会自动生成 stack 和 cabal 的配置文件。package.yaml 中定义了依赖、语言标准、默认语言拓展、库目录、可执行文件的配置和测试的配置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
name: echidna

author: Trail of Bits <echidna-dev@trailofbits.com>
maintainer: Trail of Bits <echidna-dev@trailofbits.com>

version: 2.2.1

# https://github.com/haskell/cabal/issues/4739
ghc-options: -Wall -fno-warn-orphans -O2 -optP-Wno-nonportable-include-path

dependencies:
- base
- aeson
- base16-bytestring
- binary
- bytestring
- code-page
- containers
- data-bword


language: GHC2021

default-extensions:
- DuplicateRecordFields
- LambdaCase
- MultiWayIf
- NoFieldSelectors
- OverloadedLabels
- OverloadedRecordDot
- OverloadedStrings

library:
source-dirs: lib/

when:
- condition: "!os(windows)"
cpp-options: -DINTERACTIVE_UI
dependencies:
- brick
- unix
- vty

executables:
echidna:
main: Main.hs
source-dirs: src/
dependencies: echidna
ghc-options: -threaded -with-rtsopts=-N
when:
- condition: (os(linux) || os(windows)) && flag(static)
ghc-options:
- -optl-static
- condition: os(linux) || os(windows)
ghc-options:
- -O2
- -optl-pthread
- condition: os(darwin)
extra-libraries: c++
ld-options: -Wl,-keep_dwarf_unwind
ghc-options: -fcompact-unwind
- condition: os(windows) && impl(ghc >= 9.4)
dependencies: system-cxx-std-lib
- condition: os(windows) && impl(ghc < 9.4)
extra-libraries: stdc++

tests:
echidna-testsuite:
main: Spec.hs
source-dirs: src/test
dependencies:
- echidna
- tasty
- tasty-hunit
- tasty-quickcheck
when:
- condition: (os(linux) || os(windows)) && flag(static)
ghc-options:
- -optl-static
- condition: os(linux) || os(windows)
ghc-options:
- -O2
- -optl-pthread
- condition: os(darwin)
extra-libraries: c++
ld-options: -Wl,-keep_dwarf_unwind
ghc-options: -fcompact-unwind
- condition: os(windows) && impl(ghc >= 9.4)
dependencies: system-cxx-std-lib
- condition: os(windows) && impl(ghc < 9.4)
extra-libraries: stdc++

flags:
static:
description: Pass -static to ghc when linking the stack binary.
manual: true
default: false

cabal

我基本不用,因为 stack 集成了它的功能,一些 haskell 工具可能会建议使用它安装。

Nix

nix 是很方便的环境管理工具,但是只支持 Linux 和 MacOS,有部分项目使用 Nix 来开发。nix 自己维护着一套环境,然后进入 nix shell,就可以从优先使用 nix 的环境,从而不影响用户的环境。但是比较烦恼的是,我的编辑器无法使用 Nix 的 LSP。

主要讲 flake.nix 的管理方式,因为 hevm 是这样管理的,完整文件可见:https://github.com/ethereum/hevm/blob/ba00516bfbffcf14cf11211de94901833cb7eef2/flake.nix

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
inputs = {
flake-utils.url = "github:numtide/flake-utils";
nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable";
foundry.url = "github:shazow/foundry.nix/monthly";
flake-compat = {
url = "github:edolstra/flake-compat";
flake = false;
};
solidity = {
url = "github:ethereum/solidity/1c8745c54a239d20b6fb0f79a8bd2628d779b27e";
flake = false;
};
ethereum-tests = {
url = "github:ethereum/tests/v12.2";
flake = false;
};
cabal-head = {
url = "github:haskell/cabal";
flake = false;
};
forge-std = {
url = "github:foundry-rs/forge-std";
flake = false;
};
};

input 定义了一串的依赖的来源,比如 foundry 是从 GitHub 的 shazow/foundry.nix 仓库的 monthly 分支拉取,而且不是 flake 应用。其他的类似。

outputs 函数里接收包 nixpkgs 和 inputs 中的参数:

1
outputs = { self, nixpkgs, flake-utils, solidity, forge-std, ethereum-tests, foundry, cabal-head, ... }:

然后在 flake-utils.lib.eachDefaultSystem (system: ...) 定义函数主体。

1
2
let
pkgs = (import nixpkgs { inherit system; config = { allowBroken = true; }; });

导入 nix 包,设置参数,system 参数是当前系统架构,允许包含那些被标记为不稳定或损坏的包。

1
2
3
4
5
6
7
8
9
testDeps = with pkgs; [
go-ethereum
solc
z3
cvc5
git
] ++ lib.optional (!(pkgs.stdenv.isDarwin && pkgs.stdenv.isAarch64)) [
foundry.defaultPackage.${system}
];

测试的依赖包括了上面的 5 个工具,还有一个可选的工具,当系统环境不是 ARM64 架构下的 Darwin 平台时,还会引入根据 system 参数选择的 foundry。

接着定义需要的 Haskell 的包,ghc 9.4 作为默认包的集合,然后修改部分包的配置。self 是当前配置,super 是默认的父配置,rec 允许{…}里的元素相互定义。

1
2
pkgs.haskell.packages.ghc94.override {
overrides = with pkgs.haskell.lib; self: super: rec {...};};

覆盖的部分包括,dontCheck 不运行包的测试套件,self.callCabal2nix 这个函数为 Cabal 相关的包自动生成 Nix 表达式,比如对于 cabal-install 包,从定义好的来源获取,使用默认参数。doJailbreak 忽略版本限制。

1
2
3
4
5
6
7
8
cabal-install = dontCheck (self.callCabal2nix "cabal-install" "${cabal-head}/cabal-install" {});

cabal-install-solver = dontCheck (self.callCabal2nix "cabal-install-solver" "${cabal-head}/cabal-install-solver" {});

unix = dontCheck (doJailbreak super.unix_2_8_1_1);
filepath = dontCheck (doJailbreak super.filepath_1_4_100_4);
process = dontCheck (doJailbreak super.process_1_6_17_0);

那么简单的说,用自定义的来源重新定义了 cabal 相关的包的属性,然后一些包构建的时候不运行测试,而且忽视严格的版本限制。

1
2
3
secp256k1-static = stripDylib (pkgs.secp256k1.overrideAttrs (attrs: {
configureFlags = attrs.configureFlags ++ [ "--enable-static" ];
}));

修改 secp256k1 库的属性,追加了生成静态库而不是动态库,用于构建完整可独立运行的软件。

开始准备构建 hevm 的参数,初始化处理流水线的参数,然后()里的值以此传给[]里的多个函数,比如第一个函数处理完,把结果传给第二个函数:

1
hevmUnwrapped = (with pkgs; lib.pipe (...)[..])

首先从当前目录下的 hevm.cabal 文件,生成 nix 表达式。然后依赖 secp256k1 的 C 语言密码学库。

第一个函数修改构建时 Cabal 的参数,构建时执行测试。第二个函数把 solc 等工具依赖加入测试的依赖里。第三个函数在构建时添加 -v3,输出详细日志。后面的函数也是类似的,增加了传递给 ghc 的依赖库,编译参数等。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
(haskell.lib.compose.overrideCabal (old: { testTarget = "test"; }))
(haskell.lib.compose.addTestToolDepends testDeps)
(haskell.lib.compose.appendBuildFlags ["-v3"])

(haskell.lib.compose.appendConfigureFlags (
[ "-fci"
"-O2"
"--extra-lib-dirs=${stripDylib (pkgs.gmp.override { withStatic = true; })}/lib"
"--extra-lib-dirs=${stripDylib secp256k1-static}/lib"
"--extra-lib-dirs=${stripDylib (libff.override { enableStatic = true; })}/lib"
"--extra-lib-dirs=${zlib.static}/lib"
"--extra-lib-dirs=${stripDylib (libffi.overrideAttrs (_: { dontDisableStatic = true; }))}/lib"
"--extra-lib-dirs=${stripDylib (ncurses.override { enableStatic = true; })}/lib"
]
++ lib.optionals stdenv.isLinux [
"--enable-executable-static"
# TODO: replace this with musl: https://stackoverflow.com/a/57478728
"--extra-lib-dirs=${glibc}/lib"
"--extra-lib-dirs=${glibc.static}/lib"
]))

等等这些定义好了后,在 in rec{...} 里执行编译命令。

总而言之,Nix 提供了一种专门的语法,用于描述依赖关系和构建的参数。并且它提供了全局的且独立的工具,这样能够避免环境之间的冲突。但是可以知道,很多工具它都自己编译,可能第一次运行速度会比较慢。另外,nix shell 可能需要额外的配置,才能让 vim 等软件用上它的环境。·