• 作成:
  • 更新:

EmacsのHaskellの開発環境をinteroからHaskell IDE Engineに移行しました

chrisdone/intero: Complete interactive development program for Haskell をやめて, haskell/haskell-ide-engine: The engine for haskell ide-integration. Not an IDE を使い始めました.

昔の移行記事遅まきながらEmacsのHaskell開発環境をInteroに移行しました - ncaq

昔の記事を見て気がついたのですが, 今はターゲットの切り替えは haskell-session-change-target としてhaskell-mode標準で備えていますね.

動機

以下の機能が欲しかったからです.

  • apply-refactによるhlintの推奨コードへの自動書き換え
  • 領域の自動フォーマット
  • 賢い補完
  • 新しいプロジェクトを開くたびにinteroをビルドし直すような無駄の排除
  • Language Server Protocolへの統合

インストール

デスクトップにはbcacheを使っていて容量に関しては割と富豪なので

何も考えずに

make build-all

しました. しかしロケールの問題かエラーが出ます.

もう1つのインストール方法Shakeがあるのでexperimentalですがこちらを使いました.

stack ./install.hs build-all

Emacsとの連携

公式ドキュメントにはlsp-mode lsp-ui lsp-haskellを使えと書いてありますが, eglot を使っている場合は何も考えなくて良いです.

(add-hook 'haskell-mode-hook 'eglot-ensure)

と書くだけで解決です.

ただeglotはコードのシンプル性を重視しているのでトップレベルシンボルの一覧とか使いたい場合はlsp-modeを使った方が良いかもしれません. rustic-modeがeglot推奨になったのでこっちに統一していますが…

ドキュメントを常に生成するようにする(失敗)

Haskell IDE Engineはhaddockを閲覧しているので, 使いたいプロジェクトではhaddockを作っている必要があります.

しかし, 毎回使うのにhaddockを生成するのは面倒なので, グローバルに常に使うようにしたいです.

公式ドキュメントには ~/.cabal/configdocumentation: True と書けと書いてありますが, StackのことはStackの設定で解決したくありませんか?

Configuration (project and global) - The Haskell Tool Stack

を見て,

~/.stack/config.yaml

build:
  keep-going: true
  haddock: true

と書けばOKでしたんですが…

これをするとhaskell-src-extとかのドキュメント生成ができなくなってしまって, Haskell IDE Engine自体のビルドすら出来なくなってしまいます.

haddock-library broken haddocks · Issue #3236 · commercialhaskell/stackage

Haskell IDE Engineを使いたいときだけ手動でhaddock生成する必要がありそうですね.

xmonadをstack exec経由で起動していると違うGHCのバージョンのプロジェクトで動かない問題があります

使いたいプロジェクトではエラーを出して動きません. グローバルのファイルではちゃんと動くのですが…

エラー内容はこちら.

2019-02-05 15:17:07.076481989 [ThreadId 4] - run entered for hie-wrapper(hie-wrapper) Version 0.6.0.0, Git revision 7dd6fd7ab2191734ab21b502dcf6189689196cc7 (2406 commits) x86_64 ghc-8.6.3

原因はすぐわかって, 使いたいプロジェクトではGHCのバージョンが8.2.2なのに(esqueletoが新しいLTSに対応してこなかったので), ghc-8.6.3を使おうとしています.

私はxmonadを使っていて, xmonadをstack exec -- xmonadで起動しているので環境変数がグローバル向けに弄られているのでしょう.

しかし, 普通にxmonadをstack exec無しで起動させることは出来ません.

なぜならxmonad --recompileはGHCを使ってxmonad.hsをリコンパイルするため, stackの環境変数が既に無いとコンパイルに失敗するからです.

% xmonad --recompile
XMonad will use ghc to recompile, because "/home/ncaq/.xmonad/build" does not exist.
xmonad: ghc: runProcess: runInteractiveProcess: exec: does not exist (No such file or directory)

これはxmonad --recompileを必要としないように単体でバイナリファイルを使うように書き換える必要がありそうですね.

xmonad.hsでモジュール分割をする - Qiita

を参考に書き換えていきましょう.

xmonad --recompilebuildファイルを用意することでstackを使うように書き換える手段もありますが, stack.yamlに依存関係を書くより, package.yamlに依存関係を書くほうが, チェックツールがうまく動いてくれるので単体ファイルにするのを選びました.

package.yaml

name: xmonad-ncaq
version: 0.1.0
synopsis: It is my xmonad and xmobar setting
github: ncaq/.xmonad
author: ncaq
maintainer: ncaq@ncaq.net
copyright: © ncaq
license: MIT
dependencies:
  - X11
  - base
  - classy-prelude
  - directory
  - network
  - regex-posix
  - time
  - xmonad
  - xmonad-contrib
executables:
  xmonad-ncaq:
    main: xmonad.hs
    ghc-options:
      - -Wall
      - -fwarn-tabs
      - -threaded
      - -O2
      - -with-rtsopts=-N4

書き換えました.

起動にはxmonad関数ではなくlaunch関数を使いましょう. xmonad関数は結局xmonad.hsのコンパイルを試みます.

xmobarは単体の実行アプリケーションなので, インストールスクリプトinstallに以下のように書く必要がありました.

#!/usr/bin/env zsh

set -eux

stack install . xmobar --flag xmobar:with_xft

なんかこの辺xmobarの動的依存をstack.yamlあたりに書く方法は無いんですかね. まあstack installの代わりに./installを実行するだけの違いだから別に良いか…

これで編集するたびに./installを実行する必要がありますが, まあそれが本来自然ですね.

flymakeが同じディレクトリにファイルを生成して非常にうざい

eglotはflymakeを使っているのですが, これが同じディレクトリにfoo_flymakeのように一時ファイルを生成して, しかも編集を終えても消去してくれないのでビルド対象に入ってしまって非常に鬱陶しい.

flymakeで調べてこうしろと出てきた

(setq flymake-run-in-place nil)

を設定しても関係なく出してきますし… tの方か? と思って設定してももちろんだめです.

多分古いのでもはや使えない方法なんだと思います.

でもよく考えてみるとRustだと同じくeglotでflymake使ってもこうはならないんですよね. と思って調べてみたらhaskell-mode.elが設定を書き加えていました.

(defun haskell-flymake-init ()
  "Flymake init function for Haskell.
To be added to `flymake-init-create-temp-buffer-copy'."
  (let ((checker-elts (and haskell-saved-check-command
                           (split-string haskell-saved-check-command))))
    (list (car checker-elts)
          (append (cdr checker-elts)
                  (list (flymake-init-create-temp-buffer-copy
                         'flymake-create-temp-inplace))))))

(add-to-list 'flymake-allowed-file-name-masks '("\\.l?hs\\'" haskell-flymake-init))

なるほどね.

flymake で一時ファイルの出力先をファイルと同じディレクトリにしない - Qiita を見ればわかりますがわざわざ同じディレクトリにするように変更しているようですね. Template Haskellとかの関係で同じディレクトリじゃないとエラーが出るとかのケースに対応してるんですかね.

とりあえず無効化します.

(with-eval-after-load 'haskell-mode
  (setq flymake-allowed-file-name-masks (delete '("\\.l?hs\\'" haskell-flymake-init) flymake-allowed-file-name-masks))
  )

これで解決です.

brittanyによるコードフォーマットがまともに動かない

ウリの機能の1つのコードフォーマットなのですが, まともに動きません.

フォーマットの仕方が悪いとかそういう問題ではなく謎の文字列が入ることになります.

なんでや… 多分Template Haskellと相性が悪いんだろうな…

どうやらそうらしいのでアップデートで治るまで諦めましょう.

brittany often fails on files with TemplateHaskell · Issue #206 · lspitzner/brittany

apply-refactによる自動訂正がうまく動かない

Template Haskellを使っているとまずまともに動きませんし.

使っていなくても保存のタイミングがおかしいですね… 2回実行する必要があります. これはeglotとの相性問題だったりするんでしょうか.

GHCの警告の自動訂正はうまくいくので, hlintとapply-refactとの連携がうまくいってないようですね.

補完がcompany-indent-or-complete-commonで有効にならない

company-indent-or-complete-commonという, 補完が出来れば補完, 補完が出来なければインデントするというコマンドがあります.

私はタブキーにこれを使ってきました.

company-modeでタブキーに補完もインデントも割り当てる - ncaq

しかしどうも掛け違いがあるのか, 常に補完不能と判断されてインデントされます.

色々バグを直すことなどを考えたのですが, 「補完が可能かどうか」はまだエディタが判断できる問題ではないという結論に至りました. 今回に限らず補完が不能と判断されることは多いです.

また「インデントする動作」と「補完を行う動作」というのは特になにか関連性のある操作というわけではありません.

よって素直にキーを分けることにしました.

  • Tab: インデント
  • C-Tab: モードに応じた補完
  • C-S-Tab: 単語ベースの補完

これでも既に自動補完が動き始めればTabで補完できるのでそんなに面倒ではないです.

replとエラーバッファの3分割コード

interoと大して変わらず下記で実現可能です.

というかもはやInterosとかHaskell IDE Engineとか関係ない気もしますが.

(defun haskell-repl-and-flycheck ()
  (interactive)
  (delete-other-windows)
  (flycheck-list-errors)
  (haskell-process-load-file)
  (haskell-interactive-switch)
  (split-window-below)
  (other-window 1)
  (switch-to-buffer flycheck-error-list-buffer)
  (other-window 1)
  )

interoより少しは良い

interoよりは機能豊富です.

何より一度コンパイルすれば他のプロジェクト開いてもinteroのコンパイルが不要という特徴は素晴らしいですね.

しかし既にinteroが動いている中無理して移行するまでのものだったかは微妙です. まだうまく動いていない機能も多いですし.

ただ補完は確実に賢くなっていました.

現在のEmacs Lisp

;; -*- lexical-binding: t -*-

(custom-set-variables '(haskell-stylish-on-save t))

(defun stylish-haskell-enable ()
  (interactive)
  (custom-set-variables '(haskell-stylish-on-save t)))

(defun stylish-haskell-disable ()
  (interactive)
  (custom-set-variables '(haskell-stylish-on-save nil)))

(add-hook 'haskell-mode-hook 'eglot-ensure)

(with-eval-after-load 'haskell-mode
  (setq flymake-allowed-file-name-masks (delete '("\\.l?hs\\'" haskell-flymake-init) flymake-allowed-file-name-masks))

  (define-key haskell-mode-map (kbd "C-M-z")               'haskell-repl-and-flycheck)
  (define-key haskell-mode-map (kbd "C-c C-c")             'haskell-session-change-target)
  (define-key haskell-mode-map (kbd "C-c C-l")             'haskell-process-load-file)
  (define-key haskell-mode-map (kbd "C-c C-z")             'haskell-interactive-switch-back)
  (define-key haskell-mode-map [remap indent-whole-buffer] 'haskell-mode-stylish-buffer)
  )

(defun haskell-repl-and-flycheck ()
  (interactive)
  (delete-other-windows)
  (flycheck-list-errors)
  (haskell-process-load-file)
  (haskell-interactive-switch)
  (split-window-below)
  (other-window 1)
  (switch-to-buffer flycheck-error-list-buffer)
  (other-window 1)
  )

(with-eval-after-load 'haskell-cabal (ncaq-set-key haskell-cabal-mode-map))

(defun hamlet-mode-config ()
  (local-set-key (kbd "C-m") 'newline-and-indent)
  (electric-indent-local-mode -1)
  )

(add-hook 'hamlet-mode-hook 'hamlet-mode-config)

(flycheck-add-mode 'css-csslint 'shakespeare-lucius-mode)
(flycheck-add-mode 'javascript-eslint 'shakespeare-julius-mode)