自分で使うモナドをラップする
TL;DR 使うモナドをラップしておくとモナドの変更に対して対応が楽ですよ、という話です。
シナリオ例
開発当初、開発者がこのプロダクトで使うモナドはState SomeState a
が適切である!!と考えていたとしましょう。
そうしてコードをガリガリ書いていきます。
そうするとコード中にState
が散らばります。この数をN
個としておきましょう。
func_1 :: State SomeState a
func_2 :: State SomeState a
-- ...
func_N :: State SomeState a
これが後になってExceptT SomeError (State SomeState) a
にすべきだったー!!と考え直し、先ほどのN
個の型定義を書き直していきます。
func_1 :: ExceptT SomeError (State SomeState) a
func_2 :: ExceptT SomeError (State SomeState) a
-- ...
func_N :: ExceptT SomeError (State SomeState) a
それなりに面倒です。
それから開発が進んでN
個だったモナドの散らばりはM
個になりました(N
< M
)。
そうしてまた「やっぱReader欲しいよな」、とか思ってExceptT SomeError (StateT SomeState (Reader SomeEnv)) a
と書き直したくなったとします。
M
個書き直します。
func_1 :: ExceptT SomeError (StateT SomeState (Reader SomeEnv)) a
func_2 :: ExceptT SomeError (StateT SomeState (Reader SomeEnv)) a
-- ...
func_N :: ExceptT SomeError (StateT SomeState (Reader SomeEnv)) a
-- ...
func_M :: ExceptT SomeError (StateT SomeState (Reader SomeEnv)) a
まあなかなか大変ですね。これからさらに変わるかもしれないですし。
そうしてこう考えるようになるかもしれません:
「なんてHaskellは開発しにくい言語なんだ!」
どうすればいいか
予めそのプロジェクトで使うモナドをWrapしておきます。
newtype MyApp a = MyApp { unMyApp :: State SomeState a }
deriving
( Functor
, Applicative
, Monad
, MonadState SomeState
)
N個の関数はおおよそ以下のようになります
func_1 :: MyApp a
func_2 :: MyApp a
-- ...
func_N :: MyApp a
さてExceptT SomeError (State SomeState) a
に変更したい気分になってきたとします。その場合、MyApp
自体を変更します。
newtype MyApp a = MyApp { unMyApp :: ExceptT SomeError (State SomeState) a }
deriving
( Functor
, Applicative
, Monad
, MonadError SomeError
, MonadState SomeState
)
関数自体のシグネチャは変更する必要がありません。使うモナドがMyApp
であることは変わってないからです。もちろんコンパイルエラーが出るようになればそこは直しますし、必要ならinstanceも追加します。
関数がM
個になり、またモナドを変更したい気分になってきました。
MyApp
を変更します。
newtype MyApp a = MyApp { unMyApp :: ExceptT SomeError (StateT SomeState (Reader SomeEnv)) a }
Int)) a }
deriving
( Functor
, Applicative
, Monad
, MonadError SomeError
, MonadState SomeState
, MonadReader SomeEnv
)
M
個の関数はそのままです。
func_1 :: MyApp a
func_2 :: MyApp a
-- ...
func_N :: MyApp a
-- ...
func_M :: MyApp a
楽ですね。
何が問題なのか
さてこれはHaskell特有の話ではありません。
開発中に変更がする可能性が高い箇所を見極めて(と言う程でもないけど)ラップし、局所に押し込めているだけです。
他の例を考えてみます。
あるライブラリA, Bが存在してプロジェクトでどちらを使うべきか、十分な情報がなかったとします。簡単に調査し、どちらを使っても問題なさそうなのでとりあえずAを使うことにしました。
そのプロジェクトでAをこれでもかと使います。
import A;
class Hoi {
public Integer foo() {
A a = new A();
// ...
A a = new A();
// ...
}
public String bar() {
A a = new A();
// ...
A a = new A();
// ...
}
}
その後開発が進み、ライブラリAに致命的な欠陥が発覚し、自前での修正もコストが高いとわかりました。ライブラリBではその問題はないようです。
そうしてAをBに差し替えます...大変ですね。
例えば予めAを自前のクラスでMyAでラップしておくと、AからBの差し替えも高々MyAの中の変更のみで完結します。MyAを作る(そしてAPIを整える)コストは少し増えるにしても、修正が発生しそうなら結果的にトータルでコストは減るかもしれません。その判断はドメインやライブラリや問題自体に依るでしょう。
一般化する
ライブラリの例は、外部ライブラリの依存をコード中にばらまいていることが原因です。
モナドの例も考えて一般化するなら、変更が予想される箇所をばらまいていることが原因です。
もし変更されうる箇所が割と明白であり、使用箇所を局所的に抑えられるなら、予めラップしておくと対応が楽でしょう。
もちろん至る所ラッパーだらけになるとそれはそれで面倒ですが。
動作性能面を考えると、例えばHaskellのnewtypeはコストはほとんど掛からないはずですが、一般には頻繁に呼び出されるライブラリのラッパーを作ったために必要な動作性能が達成できない、というのは問題になるかもしれません。
他にも、ライブラリが更新した時にも追従対応が楽になったりします。枯れていないライブラリを使う際のプラクティスとしてもいいのではないでしょうか。
モナドは関係ない話
ですよね?