HaskellのデファクトスタンダードであるコンパイラGHCおよびビルドツールCabalは、Package environmentという機能をサポートしている。それをrunhaskell
コマンドでHaskellスクリプトを実行するときにも活用できるようにしよう、という話。
Cabalがビルドするライブラリはインライン展開と分割リンクをサポートする都合上、同一バージョンのパッケージであっても依存やフラグの違いを考慮し異なるIDを付ける(この件について分かりやすく説明しているブログを以前読んだが、残念ながら見つけられない)。Package environment機能は、どのIDのパッケージを使用するのかをまとめたファイルであり、一度作成しておけば毎回同じ依存・同じIDのパッケージ群(これを「環境(environment)」と考える)でコンパイルできるという仕組み。
Package environmentファイルはcabal-install
ツールを使って作成する。例えば、
cabal v2-install --package-env foo --lib extra mwc-random
というコマンドを実行すると、extra
とmwc-random
とその依存ライブラリをまとめたfoo
という名前の環境が作成される(具体的なバージョンやIDは状況によって異なる)。作成されるファイルは~/.ghc/x86_64-linux-8.10.4/environments/foo
(GHCのバージョンが8.10.4の場合)。
この環境を使用してコンパイルする場合はGHCの-package-env
引数を使用する。
ghc -package-env foo Main.hs
(cabal
の引数はハイフン2個(--package-env
)だがghc
の引数は1個(-package-env
)なので注意)
これらの仕組みを使うと、例えば以下のようなHaskellソースファイルを(cabalプロジェクトを作成せずに)コンパイルできる。
cabal v2-install --package-env foo --lib extra mwc-random
-- Main.hs
import Control.Monad (replicateM)
import Data.List.Extra (takeWhileEnd)
import System.Random.MWC (createSystemRandom, uniformRM)
-- | ランダムなa-zの文字列を100文字生成し、一番最後に出現した「a」より後の文字列だけを出力する。
main :: IO ()
= do
main <- createSystemRandom
g <- replicateM 100 $ uniformRM ('a', 'z') g
str putStrLn $ takeWhileEnd (/= 'a') str
ghc -package-env foo Main.hs && ./Main
いちいちコンパイルしたくない場合、runhaskell
コマンドを使えばいきなり実行できる。
runhaskell -package-env=foo Main.hs
(runhaskell
を使用する場合は-package-env
とfoo
の間の=
が必須)
しかし、スクリプトとして使う場合shebangを使いたいが、runhaskell
に追加の引数-package-env=foo
を与えないといけないのが障害となる。
GHCのコンパイルオプションはOPTIONS_GHC
プラグマで指定できる…はずである。GHCの公式docおよびページ上のリンクを追っていくと、「dynamic」な引数に限り指定できる、-package-env
はdynamicであることがわかる。しかし、実際にやってみるとうまくいかない。さっきのMain.hs
にOPTIONS_GHC
プラグマを与えて実行してみると:
$ sed -i -e '1i{-# OPTIONS_GHC -package-env=foo #-}' Main.hs
$ runhaskell Main.hs
Main.hs:1:16: error:
unknown flag in {-# OPTIONS_GHC #-} pragma: -package-env=foo
|
1 | {-# OPTIONS_GHC -package-env=foo #-}
| ^^^^^^^^^^^^^^^^^^
そんなオプション知らないよというエラーが発生してしまう。これはGHC開発者達に認知されている、ちょっと前からあるバグのようだ。どうも直すのは一筋縄ではいかないらしい。
runhaskell-env
というわけでワークアラウンドとして、Haskellスクリプトに特殊なコメントを書いておくことで、runhaskell
に-package-env
引数を渡して実行してくれるラッパースクリプトを作成した(gist)。
やっていることは単純で、-- runhaskell-env: foo
というようなコメントをソースファイルから探し出し、runhaskell
に-package-env=foo
とスクリプトのファイルパスを渡して実行するだけというもの。このファイルを適当な$PATH
が通っているディレクトリのどれかに入れ(普通は$HOME/bin
がベストかな)、実行可能ビットを付けたら準備完了。
後はshebangと特殊コメントを書くだけ。最終的なMain.hs
の内容は以下のようになる。
#!/usr/bin/env runhaskell-env
-- runhaskell-env: foo
import Control.Monad (replicateM)
import Data.List.Extra (takeWhileEnd)
import System.Random.MWC (createSystemRandom, uniformRM)
-- | ランダムなa-zの文字列を100文字生成し、一番最後に出現した「a」より後の文字列だけを出力する。
main :: IO ()
= do
main <- createSystemRandom
g <- replicateM 100 $ uniformRM ('a', 'z') g
str putStrLn $ takeWhileEnd (/= 'a') str
これを実行する。例:
$ chmod +x Main.hs
$ ./Main.hs
uuss
(用が済んだ環境ファイルは削除してOK。)
適当に使うだけならPythonとかでいいだろうと思う一方、手軽さはプログラミング言語普及の大事な要素の一つだと思うので、私みたいにHaskellをシェルスクリプト気分で使う人にとっても便利になっていくと嬉しいな。
なお、起動ごとにめちゃ待たされることを厭わないなら、cabal
をshebangに書く方法もある。表立って説明されていないので、メインの機能ではなさそうだけど。(参考) 2021-03-21追記 cabal v2-run
の公式docにcabalスクリプトの説明がありました。