Haskell
AtCoder
stack

global projectでghc-modを使う

今更ghc-modの設定でつまずいたのでメモ。


はじめに

競プロ、流行ってますね。私も最近(でもないですが)、AtCoderで競プロをはじめました。

AtCoderではHaskellのコードも提出できるのですが、提供されているGHCは7.10.3と、少し(?)古めです。

イマドキのhaskellの開発環境はHaskell IDE Engineを使用するようですが、

残念ながらGHC7系はサポートされていないようです。

そのため、ghc-modをemacsのバックエンドとして使用して開発環境を構築することにします。

環境は以下の通り。

version

stack
1.9.3

resolver
lts-6.35 (ghc 7.10.3)

ghc-mod
5.5.0.0


ビルドしたパッケージがghc-modから認識されない

私が競プロに取り組むとき、新しいプロジェクトを作成せずglobal projectでコードを書いています。

必要なパッケージも、ビルドしています(例えば、Data.Vectorを使用するためにvectorパッケージをstack buildします)。

さて、追加でビルドしたパッケージ中のモジュールを使ってコードを作成したとき、

stack ghciなどで作成したコードをそのまま試す分には問題ないです。

ただし、emacs等の中でghc-modによる構文チェックが実行されているとき、以下のようになって追加導入モジュールが認識されません。


Maih.hs

import qualified Data.Vector as V

...

Main.hs:1:18:Could not find module `Data.Vector'Perhaps you meant Data.Functor (from base-4.8.2.0)Use -v to see a list of the files searched for.

すなわち、ghc-modからはData.Vectorが見えていません。

これは、ghc-mod check Main.hsで直接構文チェックをした時も同様です。


解決

issue

ghc-mod Failed to load interface for ‘Data.List.Split’ #878

を参考にて色々試してみた結果、GHC_PACKAGE_PATHを環境変数として以下のように設定するのが良さそうです。


.bash_profile

export GHC_PACKAGE_PATH=$(stack path --local-pkg-db):$(stack path --snapshot-pkg-db):$(stack path --global-pkg-db)



補足

注意: 以下の内容は私が検証した結果であり、上記issueで@DanielGさんが指摘している内容とは一部異なる可能性があります。

ghc-modがパッケージの存在を確認する場所は以下のディレクトリです。


  • システム領域(stackでghcをインストールする限り、通常はここは使用しない)

  • ユーザごとの領域( stack path --global-pkg-dbで表示されるパス、各ユーザの領域にインストールしたGHCに対応するパッケージDBのディレクトリ)

ghc-modはデフォルトでは上記のみ確認します。

Stackを使用してパッケージ管理する場合は、上記に加えてさらに2つのパッケージDB用ディレクトリが存在します。


  • スナップショットごとの領域(stack path --snapshot-pkg-dbで表示されるパス)

  • プロジェクトごとの領域(stack pathlocal-pkg-dbで表示されるパス、global projectもここに含まれるっぽい)

今回ビルドしたパッケージは、stack build vectorコマンドで実行しており、プロジェクト固有の(extra-depsに記載される)パッケージではないため、「スナップショットごとの領域」に登録されます。

そのため、ghc-modからは見えず、従ってemacs上でパッケージが認識されません。

一方、以下のようにした場合は事情が異なります。

# stack exec -- ghc-mod check Main.hs

stack exec コマンドは、プロジェクト内でビルドしたアプリを実行するために使うことが多いです(私はその認識でした)が、実際には、「Stackが作った環境情報を追加した環境で、stack exec以下の任意のコマンドを実行する」ためのコマンドとして理解する方が正しいようです。

stack exec lsみたいなコマンドも実行できますし、以下のように環境変数を比較すると、

# diff <(env | sort) <(stack exec env | sort)

0a1,4
> GHC_PACKAGE_PATH=/root/.stack/global-project/.stack-work/install/x86_64-linux/lts-6.35/7.10.3/pkgdb:/root/.stack/snapshots/x86_64-linux/lts-6.35/7.10.3/pkgdb:/root/.stack/programs/x86_64-linux/ghc-7.10.3/lib/ghc-7.10.3/package.conf.d
> HASKELL_DIST_DIR=.stack-work/dist/x86_64-linux/Cabal-1.22.5.0
> HASKELL_PACKAGE_SANDBOX=/root/.stack/snapshots/x86_64-linux/lts-6.35/7.10.3/pkgdb
> HASKELL_PACKAGE_SANDBOXES=/root/.stack/global-project/.stack-work/install/x86_64-linux/lts-6.35/7.10.3/pkgdb:/root/.stack/snapshots/x86_64-linux/lts-6.35/7.10.3/pkgdb:
7c11
< PATH=/root/bin:/root/.local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
---
> PATH=/root/.stack/global-project/.stack-work/install/x86_64-linux/lts-6.35/7.10.3/bin:/root/.stack/snapshots/x86_64-linux/lts-6.35/7.10.3/bin:/root/.stack/compiler-tools/x86_64-linux/ghc-7.10.3/bin:/root/.stack/programs/x86_64-linux/ghc-7.10.3/bin:/root/bin:/root/.local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
9a14
> STACK_EXE=/usr/local/bin/stack
11c16
< _=/usr/bin/env
---
> _=/usr/local/bin/stack

stack execの有無で環境変数に差が出ていることがわかります(PATHも変更され、アプリのバイナリが存在するディレクトリにPATHが通っています)。

ここで注目するのが、stack execで追加されたGHC_PACKAGE_PATH環境変数です。

この変数に格納されたパスを、ghc-modはパッケージ検索の際に使用します(この中に、スナップショットごとの領域も含まれます)。

この値は、それぞれglobal projectで実行されたstack pathの内容となっています(解決策で設定したのと同じ内容です)。


終わりに

一応、パッケージ検索用の環境変数を設定することで問題は解決しました。が、issueでは、@lierdakilさんが以下のように指摘しています。


あなたがbaseからのパッケージだけを使用する10行のスクリプトを持っているならば、すべては今のようにうまくいくでしょう。

そうでない場合は、global projectを汚染するのではなく、stack newを使用してプロジェクトを作成することをお勧めします。


これはその通りで、そもそも複数のsnapshotが並存している場合には、上に書いた解決策ではうまくいかない気がします。

私の場合は、競プロでのコーディング用にDockerでresolverを固定したイメージを作って使用しているため、上記の方法でうまくいっています。

なお、今回の設定内容はglobal projectでしか正しく動きません。

...とここまで書いといてなんですが、

stack exec -- emacs Main.hs

とかでいいような気がしてきました。

これなら何も汚さないし。

以上です。