haskellとnixの話

この記事は、Haskell Advent Calendar 2022の23日目の記事です。
だいぶ荒削りですので、間違いがあるかもしれません。


# 目的

OSSのプロジェクトで4年ほどNixを使い続けてきました。
NixのHaskellのサポートの現状のいいところと課題を整理するのが目的です。
なぜNixを使うのかを確認したあと、NixのHaskellサポートをみて課題をまとめます。

# なぜNixか、なにがやりたいのか？

Nixはパッケージマネージャーです。各種Linuxディストリビューション, MacOSに入れられるものです。
Nixに期待していることはなんでしょうか？
それは再現性と効率性ではないかと思います。

まずは再現性についてです。[nixosのポータル](https://nixos.org/)の一番はじめにでてくるように再現性がもっとも重要でしょう。
手元の開発環境やクラウドの開発環境、各種のメンバーで開発するソフトウェアのバージョンや構成を再現性のあるもので構築し、
本番環境でも同様に運用することではないでしょうか。
例えば、特定の環境だけ起こるバグが少ないほうがいいでしょう。
開発はMacOSだが、本番はLinuxで本番のバグが開発環境で再現できないとデバッグできません。
このような悩みを解決するためにNixを導入して再現性を担保することを期待しているのではないでしょうか。

Nixに対比されるDockerはDockerイメージを使うだけなら動作の再現性が高いが、イメージの構築は通常のOSと同じように個々のパッケージは
ハッシュでバージョン管理しているわけではないので、再現性は高くありません。

NixではなくHaskellの話ですが、Haskellである程度の規模のプロジェクトだと特定のGHCのバージョンに対応したパッケージセット・Stackageを使う必要があります。
Nixのデフォルトのパッケージセット(nixpkgs)は、残念ながらStackageのサポートがなく、HaskellのNix用のパッケージ管理ツールが複数あるのが現状です。

次は効率性です。
開発効率は重要です。再現性が担保できても開発効率が下がり開発速度が下がるなら競争力はありません。
Nixは気軽にパッケージが作れ、パッケージ単位でキャッシュができます。
Haskellはビルド時間が長いので個々のパッケージのコンパイル結果をパッケージとしてキャッシュしてビルド時間の短縮を期待したいところです。
また、NixはDockerのようにコンテナではなく、ホストのリソース(メモリやCPU)をもれなく使用できるところは開発効率の点で重視したいところです。

# NixのHaskellサポート

現状次の4つのNixによるHaskellのサポートがあります。
[こちらのフローチャート](https://nixos.wiki/wiki/Haskell)にあるように用途ごとにどれを使うか選ぶ必要があります。
なぜ、このように面倒なことになっているかというとそれぞれのツールにトレードオフがあり、再現性と効率性の両方を担保するものがないからです。

* [cabal2nix](https://haskell4nix.readthedocs.io/index.html)
* [stackのNix integration](https://docs.haskellstack.org/en/stable/nix_integration/)
* [haskell.nix](https://input-output-hk.github.io/haskell.nix/)
* [stacklock2nix](https://github.com/cdepillabout/stacklock2nix)

それぞれどういうものか、問題がなにか見ていきましょう。

## cabal2nix

cabal2nixはNixのデフォルトのパッケージセット(nixpkgs)の組み込みのものです。
stackより前からあるものでcabal v1でビルドされているものです。
Haskellの個々のパッケージを一つ一つ別々のパッケージにしています。
キャッシュが効いていて開発効率は悪くないのですが、Stackageが使えません。
また、複数のパッケージのバージョンを変更するのが苦手です。
そのため、簡単なプロジェクト以外では使われないです。

## stackのnix integration

stackにはNixの機能がついています。
これはHaskellのパッケージが依存するC/C++などのライブラリをNixを使ってセットアップするものです。
Haskellの個々のパッケージは一切Nixでキャッシュされていません。
また、作ったものをNixのパッケージにもできません。
GHCはNixのものではないcentos?のGHCのバイナリを使うようでlibcの周りでトラブルが発生しがちです。
開発の再現性はcabal2nixに劣り、開発効率はNixを使わない場合と違いはないように感じます。

## haskell.nix

haskell.nixはstackageを使ってstackとcabalをサポートしていて、独自のGHC(GHCJSなど)を使うことができます。
作ったものをNixのパッケージにもできます。開発の再現性は高いです。
（機能は豊富ですが、コードが読みにくく使いこなすのが難しいというのはあります。）

開発時はstackのnix integrationのようにHaskellのパッケージが依存するC/C++などのライブラリをセットアップして、ビルドします。
そのため、開発の再現性はあるものの、開発効率はNixを使わない場合と違いはありません。

本番用のビルドはcabal2nixのようにHaskellの個々のパッケージを一つ一つ別々のパッケージにします。
開発時のビルドと本番用のビルドが別で、開発者は本番リリース前に本番用のコンパイルが必要で開発の手間が増えます。
また、独自のGHCを使えますが、環境によってはGHCのビルドから開始されるので、初期環境構築に時間がかかることがあります。
そのため、開発の効率性はよくないです。

## stacklock2nix

stacklock2nixはhaskell.nixのようにstackageとstackをサポートしていて、Nixのデフォルトのパッケージセット(nixpkgs)のGHCを使います。
作ったものをNixのパッケージにもできます。開発の再現性は高いです。
haskell.nixより機能を限定しているため、コードが読みやすいです。

開発時はstackのnix integrationのようにHaskellのパッケージが依存するC/C++などのライブラリをセットアップして、ビルドします。
そのため、開発の再現性はあるものの、開発効率はNixを使わない場合と違いはありません。

haskell.nixと同様に開発時のビルドと本番用のビルドが別になり、開発者は本番リリースまえには本番用のコンパイルが必要で開発の手間が増えます。
GHCはnixpkgsのものを使うため、GHCそのもののビルドは走らず、初期環境構築がhaskell.nixより速いです。
そのため、開発の効率性はhaskell.nixよりいいという印象です。

# 現状の課題とまとめ

haskell.nixやstacklock2nixでstackageを使って再現性を担保して開発することができます。
Nixを使って開発の効率性をあげると言う点については課題が残っています。
haskell.nixやstacklock2nixは本番用のnixのパッケージを作ることはできます。
しかし、開発の効率性を上げるためにcabal2nixのようにhaskellのパッケージ単位でキャッシュをして、開発時にそのキャッシュをつかってビルドを高速化することはできません。

結局どれがいいかというのは用途によるということになります。
stacklock2nixで始めるのが無難かと思います。