最近はよく rio + extensible で Haskell アプリケーションを書きます(趣味の). 前々から何となくパターン化できそうだなぁと思っていたのが,それをついにパターン化し mix パッケージとして形にしましたというお話です.

ちなみに,それぞれのパッケージを軽く説明すると:

  • rio : Haskell のビルドツール Stack を開発しているチームが作っている Reader パターンをベースにした Alt. Prelude
  • extensible : 拡張可能レコードを始めとして様々な拡張可能なデータ構造を同一の形式で利用できるようになるパッケージ

mix パッケージ

リポジトリはこれ:

mix パッケージの目的は rio パッケージの RIO env a モナドの env の部分を extensible パッケージを用いて簡単に構築することであり,env をプラグインとして構築する. プラグインで構築という部分は tonatona から着想を得た(tonatona も rio のラッパーパッケージなはず). 例えば,rio パッケージのロガーを利用して次のような簡易的なプログラムをかける:

tonatona との違いは RIO env aenv に当たる部分に対して,特別なインスタンス宣言がいらない点だ. 単純に,設定っぽい extensible の拡張可能レコード(#logger <@=> ... とか)を記述するだけで良い. これの実行結果は次のようになる:

$ stack runghc mix/sample/Main.hs
2019-05-21 22:33:49.378471: [debug] This is debug: Hoge
@(mix/sample/Main.hs:23:3)
2019-05-21 22:33:49.381893: [info] This is info: Hoge
@(mix/sample/Main.hs:24:3)
2019-05-21 22:33:49.381943: [warn] This is warn: Hoge
@(mix/sample/Main.hs:25:3)
2019-05-21 22:33:49.382005: [error] This is error: Hoge
@(mix/sample/Main.hs:26:3)

なぜ mix ではインスタンス宣言などせずに自由にプラグインのオンオフや設定のカスタマイズをすることができるのだろうか? 言わずもがな,extensible の魔法によるものである.

extensible の魔法

もっとも鬼門になったのは rio のロガーだ. rio のロガーは次のように利用する必要がある:

withLogFunc opt の型は MonadUnliftIO m => (LogFunc -> m a) -> m a となっている. なぜこのような形になっているのかの秘密は(たぶん) MonadUnliftIO にあるのだが今回は割愛する. この型,よく見ると継続になっているのがわかるだろうか?

継続は Monad 型クラスのインスタンスなのでモナディックに扱える. そして,extensible の拡張可能レコードの特徴として レコードのフィールドをモナディックに走査できる! というのがある(正確には Applicative ですが). 例えば hsequence という関数が走査する関数だ:

実は Plugin という型はただの継続で,Mix.run plugin は単純に runContT した中で runRIO env action しているだけだ:

思いついてしまえば極めて簡単な仕組みだ(なおパフォーマンスについては特に考えていません).

プラグイン

プラグインと言ったもののただの継続だ. 今あるのは:

  • Logger
  • Config
  • API Client (GitHub, Drone)
  • Shell

だけで,ちょうど最近作ってたOSSで必要になった分だけ. そのうちDB系のやつを作ってもいいかもしれない. これらは全て mix と同じリポジトリに置いてある.

Logger と Config

この2つは mix ライブラリに入っている. Logger は上記に載せた rio の Logger のラッパー. Config というのは設定ファイルを指しているつもり. "config" フィールドと任意の型と紐づかせている:

Config は試しに作ってみたけど,いまいち使い道がない.

API Client

API クライアントを利用するのに必要な情報(API トークンなど)を env に載せて,クライアントを利用するときにほんの少しだけ簡単に利用できるプラグイン. GitHub と Drone CI のものを作った. GitHub のクライアントは github パッケージを Drone のクライアントは(僕が作った) drone パッケージを使う. 各プラグインのパッケージは mix-plugin-githubmix-plugin-drone として matsubara0507/mix.hs リポジトリに置いてある.

こんな感じに使える:

これを実行するとこんな感じ:

$ GH_TOKEN=xxx DRONE_HOST=cloud.drone.io DRONE_TOKEN=yyy stack runghc -- Main.hs
fetch GitHub user info:
Hi matsubara0507!!
fetch Drone user info:
Hi matsubara0507!!

本来は envReader モナドから取ってきて使うのを省いているだけなので,まぁ対して変わらない. 試しに実験的に作ってみただけ. インターフェースを揃えるとか,もう少し手を加えてもいいかもしれない.

Shell コマンド

shelly というパッケージを利用したシェルコマンドの実行を支援する. env にはシェルコマンドを実行したいパスを保存し,与えたシェルコマンドを cd した上で実行してくれる:

おしまい

過去のツールをこれで mix で置き換えていきたい2019です. ちなみにパッケージの名前は現在(2019/5)所属してる社名から(せっかく入社したならって気分).