Haskell開発の勘所2023

2023/12/11に公開
2

Haskellはいいぞ。

Haskell自体はいい言語なんですが、歴史的な理由もあってプロジェクト管理に癖があるのでその話をします。

ツールのインストール

開発ツールとしては

  1. コンパイラ: GHC
  2. ビルド: Cabal, Stack
  3. 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.cabalpackage.yamlに基づいて自動で更新します。先述したようにcabalファイルは書きづらいけどcabalは捨てられないのでこのような仕組みになっています。gitignoreに*.cabalを追加してしまっていいです。 手動でcabalファイルをいじる必要はありませんが、生成されたcabalファイルをgitにコミットしておくことが推奨されています。

package.yaml

package.yamlhpackというフォーマットに基づいています。仕様がわからなくなったらここを見に行ってください。cabalファイルは同じことを何回も指定しなければいけずめんどくさいのですが、hpackはDRYにかけていい感じです。

重要なトップレベルフィールドとして、

  1. library
  2. executable(s)
  3. tests
  4. benchmarks

の4つがあります。libraryはその名の通り、Haskellコードとして再利用可能なライブラリの設定(依存ライブラリなど)を記述します。

executablesには実行可能なバイナリファイルをビルドするための設定を記述します。libraryとは別に依存パッケージを記述できるので、例えば CLIオプションをパースするパッケージをlibraryの依存に含めない(->ビルド時間短縮)ということができます。

testsbenchmarksの実態はexecutablesです。testsbenchmarksも、テストとベンチマークのための実行可能バイナリをどうやってビルドするかという設定を記述するに過ぎません。例えば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つのモジュールを持つパッケージを考えます。

  1. Xxx.A
  2. Xxx.B
  3. Xxx.Internal

Xxx.AXxx.Bだけをexposed-moduleに書くとXxx.Internalは外部から参照不可能になります。パッケージの利用者が使用しないモジュールを隠すことでわかりやすくなります。

ちなみにcabalだと隠すモジュールも全て列挙しなければいけないのですが、hpackだとそこはいい感じにしてくれます。便利ですね。

executables#main

executables#name#mainフィールドには、実行可能バイナリのエントリーポイントを指定します。

少し注意なのですが、package.yamlのトップレベルにはexcutablesexecutableフィールドの両方が存在します。前者を使うと複数の実行可能バイナリをビルドできます。後者は単一の実行可能バイナリをビルドするときに使います。

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の使い方を説明するだけになってしまった。

脚注
  1. コメントにて誤りであると指摘をいただきました ↩︎

Discussion

YAMAMOTO YujiYAMAMOTO Yuji

Cabalだけでも一応プロジェクト管理はできるのですが、設定ファイル(xxx.cabal)が独自記法で書きづらいとか、複数パッケージの管理ができない、エラーメッセージがわかりづらいなどの問題があります。

cabal.project ファイルを使えば複数のパッケージを含むプロジェクトの管理はできますよ。
確かにエラーメッセージのわかりづらさとかstackより劣る点はありますが、昔に比べてcabalとstackの差はずいぶん縮まりましたし、cabalの方が優れたところもたくさんあります。
詳しくは(これもすでに古いですが) https://the.igreque.info/slides/2019-11-29-stack-cabal#(1) をご覧ください。

ararkarark

すいません!私の調査不足でした。修正します。ご指摘ありがとうございます!