GUI 付きのツールを作成したい時、普段は Elm を使います. さらにバックエンドを付けるときは Haskell + Elm で実装し,Elm 側で呼び出す API クライアントは elm-exportservant-elm というパッケージを使い,Haskell Servant の API 定義から自動生成させていました.

elm-export は Elm 0.19 に追いつけてなかったので独自にフォークして運用し,また愛用している extensible パッケージにも独自で対応していました. しかし,servant-elm が Elm 0.19 に対応するために依存パッケージを elm-export から elm-bridge に変更したところ,独自で対応してた部分と色々都合が悪かったので,それらのクッションとなる薄いラッパーパッケージ群として elmap.hs プロジェクトを作成しました:

この記事は elmap.hs を作る上で色々したことのメモ書きです.

なぜ elm-bridge/servant-elm ではダメか

を説明するためにまずは elm-bridge パッケージについて紹介する.

elm-bridge パッケージ

elm-bridge は Haskell の型定義から Elm の型定義などを生成するためのパッケージだ. 型定義の他にも,Elm 側の JSON デコーダー・エンコーダーも自動生成してくれる(メタプログラミングなどが無い Elm にとってこれは大変嬉しい).

仕組みは簡単で,簡易的な Elm の型定義などを Haskell で表現している.

Elm での型定義の構文を表したのが ETypeDef 型で,関数の型を書くときなどの型を表現する構文を表したのが EType 型だ. toElmType 関数を使うことで任意の型の ETypeTypeable で機械的に導出したり,deriveElmDef と TemplateHaskell で ETypeDef への変換を導出することができる:

ちなみに deriveElmDef の代わりに deriveBoth を使うと aeson 系の型クラスのインスタンスも一緒に導出してくれる. そして ETypeDef などに変換したあとは,renderElm 関数や makeElmModule 関数を使うことで Elm のコードに変換できる.

都合の悪い部分

問題はどっちかというと servant-elm にある. servant-elm は Elm 関数の変換に内部で toElmType 関数を使っている:

ここで,extensible パッケージの拡張可能レコード型に toElmType を使ってみると…

これは絶対に Elm 側で動かない… そもそも拡張可能レコードは型エイリアスで定義されているため型名の情報が落ちてしまうし,理想的には Elm のレコード型に変換してほしい.

ということで,extensible の拡張可能レコードでもうまく動作する薄いラッパーパッケージを作ることにした.

elmap.hs プロジェクト

elm-bridge と servant-elm には生成した ETypeDefEType を再変換する関数を指定することができる:

しかし,これで extensible の型を変換しなおすのは大変なので toElmType のところから置き換える必要がある(そもそも型名の情報が落ちてしまっているし).

elmap パッケージ

toElmType が扱いにくい理由は,Typeable 型クラスから自動で定義を導出されてしまうからだ. 多くの場合,自動で導出される方が嬉しいのだが,今回は型名の情報を与えたり Elm のレコード型に対応させたりなど,個別に対応したいので型クラスを利用することにする:

さらに,リネームを簡単に行いやすいようにリネームする関数を定義しておいた:

servant-elmap パッケージ

servant 側では elmap パッケージで定義した IsElmType 型クラスを利用するようにする:

getEndpoints 関数を書き換えたので,この関数に依存している関数を全部置き換えた(ということをする必要があったのでフォークせずに薄いラッパーパッケージを作るようにした).

extensible-elmap パッケージ

最後に,extensible の拡張可能レコード型に対して IsElmType 型クラスのインスタンスを定義すれば完成. 完成品はこちら:

ElmType 自体は型名を覚えるだけなので簡単(とはいえ,この定義だと型変数などには未対応だが…). さらに IsElmDefinition 型クラスのインスタンスも定義する:

多少ボイラーテンプレートができてしまったが,まぁ個人的には許容範囲.

おまけ: 普通のレコードに対応する

僕はほとんど使わないからいいんだけど,普通のレコード型も IsElmType に対応しやすいようなヘルパー関数を定義しておこう:

toElmAlias 関数では,Haskell の総称プログラミング(Generics)という機能を利用している. Generics では Haskell 内での型や値のメタ情報を Haskell の型や値として取得できる. これと型クラスを組み合わせることで,型の構造毎に関数の実装を分岐することができる:

おしまい

もう少し簡単に解決する方法もあったような気もするけど,Haskell のメタプログラミングを色々味わえたのでまぁいいや. どうせ,僕ぐらいしか使わないだろうし(笑)