Freeモナドを活用して問題を記述する[WIP]
2016-03-20 / [haskell]
ここで書くこと
Haskellでここ1年くらいずーっと悩んでいた問題が少し解けてきている気がするので、その問題の捉え方と解決までの歩みについて書きます。
同じことで悩む人の参考になれば幸いです。
問題とは……
ドメイン(問題領域)のロジックがHaskellらしく書けない ということです。
私が思う"Haskellらしい"コード : - 純粋なコードと副作用を持つコードが 8:2 くらいの比率で書かれている
- 型によって問題が分解されて表現されている
- fugafuga
私が思う"Haskellらしくない"コード
: - 至るところにIO
モナドが跋扈しているコード
- 関数の中にたくさんの文脈が含まれている
こんな人に読んでほしい
『すごいHaskellたのしく学ぼう!』を読み終わったくらいの人。
- MaybeやListモナドの存在を知っている
キーワード
- Free Monad
- 純粋なドメイン
参考にした書籍, 記事
今の理解にたどり着くまでに読んだ書籍・記事の中で特に腑に落ちたものを列挙しておきます。
Purify code using free monads : IOまみれになりがちなコードを純粋に保つための方法を教えてくれました
5 Ways to Test Applications that Access a Database in Haskell : コードを純粋に保つことでどんな恩恵があるかを教えてくれました
Scala関数型デザイン&プログラミング - 13.4.1 フリーモナド : ドメインの翻訳(translation)という考え方について教えてくれました
【型レベルWeb DSL】 Servantの紹介 : 型を使って問題を表現することの好例を知りました
Understanding F-Algebras : 関手をベースにドメインを体系化する方法として参考にしました
要点
余計なレイヤを含まない純粋な問題領域を作る
これに尽きます。問題領域を以降ではドメインと呼びます。
何を言っているかというと、DBとかJSONとかHTTPとかそういう外の世界のルールに依存したものを 徹底的に排除したデータ型の空間を作り上げることです。このデータ型の空間がドメインとなります。
ドメインが外部作用に一切依存しないように設計します。 従来はドメインとインフラが垂直に統合されており、インタフェースの実装をDIで与えることで
垂直的な関係による依存
Application
↓ depends
Domain
↓ depends
Infrastructure
水平的な関係による変換
Applicationはドメインが受理可能なイベントの集合を定義する。 Domainはイベントを解釈してアプリケーションの状態を書き換える。 Domainが生成したコマンドのツリーはInfrastructureで解釈され実際のミドルウェアへの操作に変換される。
Applicationはユースケースレベルの抽象度を表し、 DomainはBounded Contextレベルの抽象度での操作を定義し、 Infrastructureは実装されたミドルウェアレベルの操作を定義する。 (このレベル自体は従来型の垂直的アーキテクチャと変わらない)
translates translates run
Application ----------> Domain ----------> Infrastructure -----> IO
depends depends
----------> <----------
ドメインをピュアに保つ恩恵
T.B.D.
設計上のブレイクスルー
その1 インタフェースをデータ型として定義する
オブジェクト指向プログラミングではデータにメソッドを生やすことで手続きを記述していました。
私はこの思考から抜け出しきれずしばらく放浪していました。
そのため構造体のようなレコードとメソッドの代わりとなる関数をひたすら書いていました。 結果的に起きたのはデータ型に対してどんどん増えていくバラバラの関数です。
Object = structure + methods
↓
Functional way = record + functions
それぞれの関数の型注釈はそこそこ上手く事前条件と事後条件を表しているのです。 しかしそれらがバラバラと増えていくと目的のために必要な関数を見つけるのにもだんだん苦労するようになってきます。
オブジェクトベースの時はクラスにメソッドが集約されていましたが、 今ではレコードと関数がバラバラに定義されています。状況はより悪くなったようにすら感じます。
その2 理想化する
T.B.D.
考え方
イベントを受信したらそれに基づいく命令の木を作る。 命令の木を簡約して値を生成する。 簡約時にシステムの状態を書き換える。
型で考える前に自然言語で考える。スケッチをする。 モノとモノの関係を記述する。
今のところの設計フローメモ(T.B.D)
ユースケースの整理
ものごとを記述する場合にもっとも簡単なのは、 時系列にしたがって起こるイベントを捉えることだ。
- チケットを作成する
- チケットの担当者をAにする
- チケットにコメントを追加する
- チケットをクローズする
イベントはXをYするのように述語として表すことができる。
イベントは連続する。
-
値を表すデータ型を定義する(モノのエンコード)
-
ユースケースをドメイン内のイベントとして記述しきる(コトのエンコード) ユースケースは複数の操作に分解できるので、それらは後回し
-
プリミティブを作る プリミティブを合成してユースケースを作ると考える これがDSLのASTになる
-
プリミティブを組み合わせてドメインのユースケースを作る
-
組み合わせて式を作る
-
specを書いて使い方をつかむ
-
7/7
- 混乱してきたので基本的な事項の確認
- まずASTがFunctorでなくてもよいものか?
- とある言語を作っていると考えると構文木と書き換え規則(関数)が必要
- ASTは操作の組み合わせを定義する
- すべての式を代数式として組み上げてからliftすることで代数系の意味論を崩さずにすむ
- 混乱してきたので基本的な事項の確認
-
7/8
- primitiveとcombinatorという考え方
- primitiveは構文木の基底部分
- combinatorは実質的に高階関数
- ADTとして定義するべきものは
Expr
のような式の構文だと思っていたが違うのか?- 再帰的なデータを素直に定義するとFreerでliftしにくくなる
-
7/9
- 今気持ち悪いと思うのは代数系とモナドが混ざること
- モナドに統一するためADTのコンストラクタの再帰を取り除いた
- わかってきたのは再帰のあるAST的なものはFreeにはならないということ
- これはFree自体が一般化された再帰を実装しているため
- 一方のGADTベースの再帰はFreeなFunctorなしevalのみあるような構成の時に使える
- bindやfmapを使いたい場合は自動的にFree化する必要があるため、再帰ADTは捨てる、
- というのが今の理解
- DSLのASTには失敗する可能性を表した
Maybe
も入れていたが要らないことがわかった - 純粋なドメイン上での操作には失敗の文脈が入っては行けない
- fetchでキーを渡したら取れるはず、と仮定していい
- 取れないならばそれは404エラーとするしそれはinterpreterによってreifyする時の仕事だ
肥大化する関数
レイヤの分離
ドメインを純粋に保つ
モノとコトとKind
ダメだったやりかた
抽象的なデザインだけでなくて
例えば下記のようなことも考える必要がある。
- ログ出力
- 例外処理
- エラーメッセージ
- 多言語化
ビジネスロジックは抽象的にしたいのでinterpreterでケアさせるけど