Haskell开发环境搭建

两年之前撰文 Haskell的Atom IDE伪装,那时刚离开英国,因为在旁听一门大一新生的函数式编程入门课时某个契机启发了主讲教授,便委托我撰写了一个配置指南。期间我们还因为依赖的安装互相通过邮件交流了很多。现在当我再次去网站上浏览时发觉atom是仅剩的指导了,说来惭愧,我现在不怎么用atom了,使用atom取代emacs并不是我的本意,至少我并不希望任何一款editor占据主导地位。

2020年1月补充:随着时间的推移,我从在Aquamacs上使用 Ghc-modintero 再到 Dante,以及VS Code配合 Haskell IDE Engine,可以看出整个社区生态的一个趋势:集成度越来越高。以往我们是要将各种插件统统由自己进行组装定制。现在有了LSP,慢慢后端变得整齐多了。我个人是比较倾向多使用现有成熟工具体系的,有时候面对很多新锐语言只能自己组环境时才去用Vim/Emacs。对于Haskell来说,至少目前HIE大有前途,而且维护也很活跃,兴许用不了很久杀手级的IDE就会诞生。

Minimal Emacs for Haskell

做工具的主人,而不是奴隶

我始终觉得工具越是开箱即用越好,曾几何时,这个赞誉属于过 Macbook。在开发工具领域,很多优质的商业化集成开发环境就是好的样本,诚然这样的工具并不是针对每一种编程语言都随处可见。不同的语言需要不同的理解和优化,好的IDE需要投入巨大的成本去开发。所以很多小众的语言依然需要我们自行配置开发环境。

Emacs for Haskell

我只需要为Haskell做最基础的配置,尽量做一个简洁的环境出来,所以这里不选择「Spacemacs」。我这里打算使用的是 Aquamacs,这是一个专门为Mac改良的Emacs,当作一个特别的发行版好了。

首先,我很久没有管理我之前的配置了,这次突然又折腾一遍是因为我买了台新的「Macbook Pro 15’」,我习惯第一时间先折腾环境。

然后我开始了「吃苦」的旅程。

Brew

不知道是GFW的原因还是被M$收购的原因,不在终端配个export ALL_PROXY都连不上Github

Stack

Stack的官方软件源实在是不能用,我换了清华的源后好多了。有了Stack当然能方便不少,但是呢,老问题来了,我16年的时候开发网站就遇到过的:版本兼容。有些包还是不能在每一个resolver里面都有,我的办法是找不到的用cabal install,除此外别无他法。

Emacs

这个槽点就多了,首先在某个版本后,把什么gnutls的插件取消了,然后我不得不疯狂Goolge:自己搞了个SSL,然后配置上了。接着呢,在Melpa中很多插件在网站列表里有,但是我本地怎么都拉不到(flycheck),所幸遇到神器intero,不然我可能就要用一个简陋版本了,解决办法是切换到清华的源,现在我已经弃用intero了,直接用Dante组装出一个最小化的版本,Dante的优势是无需安装Haskell依赖。注意使用代码补全的时候,有时候要等待TAGS文件准备好,Catalina这一版系统跑Emacs确实比较慢。

完整的配置文档

先装依赖:

1
2
3
4
5
$ brew install gnutls libressl
$ stack install happy apply-refact hlint stylish-haskell hasktags hoogle
$ git clone --recursive [email protected]:ucsd-progsys/liquidhaskell.git
$ cd liquidhaskell
$ stack install #还需要装Z3 SMT Solver,具体看官方指南

而后是配置文档(完整见此),快捷键配置需参考注释,常用如下:

  • C-c C-l 打开repl
  • C-c h Hoogle
  • M-x speedbar 打开speedbar
  • C-c r 接受hlint光标所在位置的建议,自动重构
  • C-c b 接受hlint全局的建议,自动重构
  • M-s M-s stack mode
  • M-. 跳转到定义(需先保存文件,保存文件会自动触发tags和stylish)
  • M-? 跳转到使用
  • liquid /path/to/file.hs 在VS Code配合HIE使用,或Emacs/Vim执行Unix终端命令
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
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
;; stack install happy apply-refact hlint stylish-haskell hasktags hoogle
;; for SSL issue: https://github.com/davidswelt/aquamacs-emacs/issues/133

;; M-x speedbar
(require 'speedbar)
(speedbar-add-supported-extension ".hs")

;; Prerequisite - begin

(defun hlint-refactor-call-process-region-checked (start end program &optional args)
"Send text from START to END to PROGRAM with ARGS.
This is a wrapper around `call-process-region' that doesn't replace
the region with the output of PROGRAM if it returned a non-zero
exit code."
(let ((exit (apply 'call-process-region
start end
program ; name of program
t ; delete region
t ; send output to buffer
nil ; no redisplay during output
args
)))
(unless (eq exit 0) (primitive-undo 1 buffer-undo-list))))


(defun hlint-refactor-call-process-region-preserve-point (start end program &optional args)
"Send text from START to END to PROGRAM with ARGS preserving the point.
This uses `call-process-region-checked' internally."
(let ((line (line-number-at-pos))
(column (current-column)))
(hlint-refactor-call-process-region-checked start end program args)
(goto-line line)
(move-to-column column)))

;;;###autoload
(defun hlint-refactor-refactor-buffer (&optional args)
"Apply all hlint suggestions in the current buffer.
ARGS specifies additional arguments that are passed to hlint."
(interactive)
(hlint-refactor-call-process-region-preserve-point
(point-min)
(point-max)
"hlint"
(append '("--refactor"
"-")
args)))

;;;###autoload
(defun hlint-refactor-refactor-at-point ()
"Apply the hlint suggestion at point."
(interactive)
(let ((col (number-to-string (+ 1 (current-column))))
(line (number-to-string (line-number-at-pos))))
(hlint-refactor-refactor-buffer
(list (concat "--refactor-options=--pos " line "," col)))))

;;;###autoload
(define-minor-mode hlint-refactor-mode
"Automatically apply hlint suggestions"
:lighter " hlint-refactor"
:keymap (let ((map (make-sparse-keymap)))
(define-key map "\C-cb" 'hlint-refactor-refactor-buffer)
(define-key map "\C-cr" 'hlint-refactor-refactor-at-point)
map))

(provide 'hlint-refactor)

;;Pre - end

(package-install 'exec-path-from-shell)
(exec-path-from-shell-initialize)

(defvar prelude-packages
'(haskell-mode)
"A list of packages to ensure are installed at launch.")

(defun prelude-packages-installed-p ()
(loop for p in prelude-packages
when (not (package-installed-p p)) do (return nil)
finally (return t)))

(if (version<= "24.0" emacs-version)
(unless (prelude-packages-installed-p)
;; check for new packages (package versions)
(message "%s" "Emacs Prelude is now refreshing its package database...")
(package-refresh-contents)
(message "%s" " done.")
;; install the missing packages
(dolist (p prelude-packages)
(when (not (package-installed-p p))
(package-install p)))))

;; Haskell
(package-install 'flycheck-color-mode-line)
(package-install 'flycheck-pos-tip)
(package-install 'dante)

;; COMPLETION

;; (add-hook 'after-init-hook 'global-company-mode)
;shortcut for completion
(global-set-key (kbd "C-c w") 'company-complete)

;after how many letters do we want to get completion tips? 1 means from the first letter
(setq company-minimum-prefix-length 1)
(setq company-dabbrev-downcase 0)
;after how long of no keys should we get the completion tips? in seconds
(setq company-idle-delay 0.1)

;; ERRORS ON THE FLY

(require 'flycheck)
;; (add-hook 'after-init-hook 'global-flycheck-mode)
(require 'flycheck-color-mode-line)

;tooltip errors
(require 'flycheck-pos-tip)
(with-eval-after-load 'flycheck
(flycheck-pos-tip-mode))

(setq flycheck-pos-tip-timeout 60)

(with-eval-after-load 'flycheck
(setq-default flycheck-disabled-checkers '(emacs-lisp-checkdoc)))

(require 'flycheck-color-mode-line)
(add-hook 'flycheck-mode-hook
'flycheck-color-mode-line-mode)

(global-set-key [f9] 'flycheck-list-errors)


(package-install 'flymake-hlint)
(require 'flymake-hlint)

(defun my-haskell-hook ()
(progn
(hlint-refactor-mode)
(interactive-haskell-mode)
(haskell-doc-mode)
(haskell-indentation-mode)
(dante-mode)
(flycheck-mode)
(flymake-hlint-load)
(company-mode)
))

(add-hook 'haskell-mode-hook 'my-haskell-hook)
(setq dante-repl-command-line '("stack" "repl" dante-target))
;; Put following line in your project: .dir-locals.el
;; ((nil . ((dante-methods . (stack)))))

;; (setq flycheck-check-syntax-automatically '(save mode-enabled))
;; (add-hook 'dante-mode-hook
;; '(lambda () (flycheck-add-next-checker 'haskell-dante
;; '(warning . haskell-hlint))))


;; install stack mode with shortcut: Alt-s Alt-s
(package-install 'hasky-stack)
(global-set-key (kbd "M-s M-s") #'hasky-stack-execute)

;; use Shift-arrow keys to move between windows
(windmove-default-keybindings)

(require 'haskell-mode)
(define-key haskell-mode-map "\C-ch" 'haskell-hoogle)
;(setq haskell-hoogle-command "hoogle")

;; use M-. on a name in a Haskell buffer which will jump directly to its definition
(setq haskell-tags-on-save t)

(custom-set-variables
;; custom-set-variables was added by Custom.
;; If you edit it by hand, you could mess it up, so be careful.
;; Your init file should contain only one such instance.
;; If there is more than one, they won't work right.
'(haskell-stylish-on-save t)
'(package-selected-packages
(quote
(auto-complete merlin utop tuareg hasky-stack exec-path-from-shell haskell-mode flycheck company))))

总结

效果如下图所示,hlint代码建议:

hlint 与 haskell mode 配合

代码补全,有了intero可以不需要ghc-mod:

intero 的 autocomplete

2019年补充

Visual Studio Code可以说已经取代了曾经Atom的地位,而且随着生态的不断完善已经可以胜任基本的开发工作。有一种可以说「一键式」的开发环境就是利用Haskell Language Server搭配任意你喜欢的Editor。这里我配置了一个VS Code的环境,HIE相对来讲配置简单,可以参考官方指南。相比于我自己的配置,最明显的多了HaReLiquidHaskell两个神器,这些全部配套起来再加上stack ghci完全可以作为胜任生产环境的工业级的IDE。使用效果:

VS Code + Haskell Language Server

集成LiquidHaskell做形式验证的效果:

LiquidHaskell

配置过程中可能遇到的问题切记HIE需要在stack工程下才能运行,不支持plain的项目。还有需要安装awk通过brew install awk。可以配合其他VS Code的hlint插件使用。

附:有一款为Mac开发的付费App推荐:Haskell for Mac