Haskell開発の勘所2023
Haskellはいいぞ。
Haskell自体はいい言語なんですが、歴史的な理由もあってプロジェクト管理に癖があるのでその話をします。
ツールのインストール
開発ツールとしては
- コンパイラ: GHC
- ビルド: Cabal, Stack
- Language Server: HLS
の4つが必要です。全部ghcupに任せてください。y押してるだけでいいです。
プロジェクトの作成
次にプロジェクトを作ります。Haskellのプロジェクト管理にはCabalとStackが使われます。
Cabalは昔から使われてきたプロジェクト管理ツールです。プロジェクトのビルド、依存関係のインストール、テストやベンチマークの実行などができます。
Cabalだけでも一応プロジェクト管理はできるのですが、設定ファイル(xxx.cabal
)が独自記法で書きづらいとか、複数パッケージの管理ができない、[1]エラーメッセージがわかりづらいなどの問題があります。
そこでCabalの欠点を補うために開発されたのがStackです。Stackは内部的にCabalを使っています。Stackを使うとyamlで設定が書けるし、複数パッケージも管理しやすいです。そういうわけでCabalもStackも必要なのです。
プロジェクトの初期化はstack init $package_name
で行います。こうすると$package_name/
以下にパッケージを初期化してくれます。
いくつか重要な設定ファイルがあるので解説します。
stack.yaml
stackにはプロジェクトとパッケージという概念があります。プロジェクトは複数パッケージを管理下に置けます。Web開発の文脈でいうモノレポみたいな感じです多分。
$package_name/stack.yaml
にプロジェクト全体の設定を書きます。色々な設定ができるんですが、重要なのはpackages
フィールドです。
packagesフィールドには各パッケージへの相対パスをリストで記述します。おそらく初期状態では
packages:
- .
となっていると思います。これはつまりプロジェクトとパッケージのディレクトリが一致しているということです。プロジェクトとパッケージの設定ファイルが混在するので正直あんまり好きではないです。
もしこのプロジェクトに新しくパッケージを登録する時には、そのパッケージまでの相対パスを追加します。新しいパッケージへのパスが./hogehoge/
の場合は以下のようになります。
packages:
- .
- ./hogehoge/
package.yaml
と$package_name.cabal
次にパッケージの設定に移ります。$package_name/package.yaml
と$package_name/$package_name.cabal
のどちらもパッケージの設定を記述します。2つも同じ役割の設定ファイルがあって紛らわしいのですが、端的にいうと$package_name.cabal
は全く気にしなくていいです。
stackはパッケージをビルドするときに$pacakge_name.cabal
をpackage.yaml
に基づいて自動で更新します。先述したようにcabalファイルは書きづらいけどcabalは捨てられないのでこのような仕組みになっています。gitignoreに 手動でcabalファイルをいじる必要はありませんが、生成されたcabalファイルをgitにコミットしておくことが推奨されています。*.cabal
を追加してしまっていいです。
package.yaml
package.yaml
はhpackというフォーマットに基づいています。仕様がわからなくなったらここを見に行ってください。cabalファイルは同じことを何回も指定しなければいけずめんどくさいのですが、hpackはDRYにかけていい感じです。
重要なトップレベルフィールドとして、
- library
- executable(s)
- tests
- benchmarks
の4つがあります。library
はその名の通り、Haskellコードとして再利用可能なライブラリの設定(依存ライブラリなど)を記述します。
executables
には実行可能なバイナリファイルをビルドするための設定を記述します。libraryとは別に依存パッケージを記述できるので、例えば CLIオプションをパースするパッケージをlibraryの依存に含めない(->ビルド時間短縮)ということができます。
tests
とbenchmarks
の実態はexecutables
です。tests
もbenchmarks
も、テストとベンチマークのための実行可能バイナリをどうやってビルドするかという設定を記述するに過ぎません。例えばbenchmarks
にあるコードの実行時間を自動で計測するとか、tests
にあるコードを自動でテストするかといった便利機能は一切ありません。executables
フィールドに書いたとしても同じですが、わかりやすさのために別れているだけです。
重要な共通フィールド
この節ではhpackのcommon fieldsの解説をします。
common fieldsとは、libraryにもexecutablesにも、testsにもbenchmarksにもかけるフィールドのことです。
まずはsource-dirs
フィールドです。このフィールドに指定したディレクトリのみをビルド時に参照できます。これを使ってlibraryとexecutablesでディレクトリを分けるのはよくあるテクニックです。
stack new
でプロジェクトを初期化した際も、$pacakge_name/src
と$package_name/app
の2つのディレクトリが生成されます。前者がlibraryのソースコードを置く場所で、後者がexecutablesのコードを置く場所です。
次にdependencies
フィールドです。ここには依存パッケージとそのバージョンを書きます。ちなみに、パーサージェネレータとかFFI bindingなどでビルド時だけ必要な依存パッケージが必要なこともあるのですが、そういうツールはbuild-tools
に指定します。
library#exposed-modules
library
にはexposed-module
というフィールドが存在します。これは公開するモジュールを指定するフィールドです。
例えば以下の3つのモジュールを持つパッケージを考えます。
- Xxx.A
- Xxx.B
- Xxx.Internal
Xxx.A
とXxx.B
だけをexposed-module
に書くとXxx.Internal
は外部から参照不可能になります。パッケージの利用者が使用しないモジュールを隠すことでわかりやすくなります。
ちなみにcabalだと隠すモジュールも全て列挙しなければいけないのですが、hpackだとそこはいい感じにしてくれます。便利ですね。
executables#main
executables#name#main
フィールドには、実行可能バイナリのエントリーポイントを指定します。
少し注意なのですが、package.yaml
のトップレベルにはexcutables
とexecutable
フィールドの両方が存在します。前者を使うと複数の実行可能バイナリをビルドできます。後者は単一の実行可能バイナリをビルドするときに使います。
executableのmainフィールドには、実行可能バイナリのエントリーポイントを指定します。例えば$package_name/app/App.hs
にエントリーポイントがある場合は以下のようになります。
executable:
main: App.hs
-- $package_name/app/App.hs
module Main(main) where
main = putStrLn "Hello, world!"
通常、Haskellのモジュールとファイル名は一致させなければいけませんが、main
に指定したファイルは例外です。からなずMain
モジュールを定義して、main :: IO ()
関数を書きます。この例ではファイル名はApp.hs
なのに、App
モジュールではなくMain
モジュールを定義しています。
testsとbenchmarksフィールドも実態はexecutableの別名なだけなので同様です。
まとめ
結局stackの使い方を説明するだけになってしまった。
-
コメントにて誤りであると指摘をいただきました ↩︎
Discussion
cabal.project ファイルを使えば複数のパッケージを含むプロジェクトの管理はできますよ。
確かにエラーメッセージのわかりづらさとかstackより劣る点はありますが、昔に比べてcabalとstackの差はずいぶん縮まりましたし、cabalの方が優れたところもたくさんあります。
詳しくは(これもすでに古いですが) https://the.igreque.info/slides/2019-11-29-stack-cabal#(1) をご覧ください。
すいません!私の調査不足でした。修正します。ご指摘ありがとうございます!