状態の合成比較:モナド変換子 vs Eff vs Classy
前回の記事について、実際に他の方法と比較してみたいという声を頂いたので、それぞれ同じような事をするためのコードを書き下してみました。
オーバーヘッドとかそのへんのパフォーマンス周りについては調べてないです。 きっと誰かがやってくれるさ。
モナド変換子の場合
module Main where import Control.Monad.State data Foo = Foo { foostr :: String , fooint :: Int } deriving Show data Bar = Bar { barstr :: String , barint :: Int } deriving Show ---- changeFoo :: State Foo () changeFoo = modify $ \foo -> foo { foostr = "NYAN" } changeFooBar :: StateT Foo (State Bar) () changeFooBar = do modify $ \foo -> execState changeFoo foo lift . modify $ \bar -> bar { barint = 100 } main = do let foo = Foo { foostr = "hoge", fooint = 1 } let bar = Bar { barstr = "piyo", barint = 2 } print (foo, bar) print $ execState changeFoo foo print $ runState (execStateT changeFooBar foo) bar
こうしてみると、State同士をモナド変換子で合成するのはとても良い設計とは言えない感じ。
changeFooBar関数の中でexecStateを実行しなくちゃいけないのもダサいですし、なにより、liftが付きまとうのは辛いですね・・・。状態の数が3つ以上になったら、一旦一つの型にまとめたほうが良さそうです。4つとか5つくらいのStateモナドを合成して扱うなんてなったら、脳みそが付いていける自信無い・・・。
Extensible Effects
{-# LANGUAGE FlexibleContexts, DeriveDataTypeable #-} module Main where import Data.Typeable import Control.Eff import Control.Eff.State.Lazy data Foo = Foo { foostr :: String , fooint :: Int } deriving (Show, Typeable) data Bar = Bar { barstr :: String , barint :: Int } deriving (Show, Typeable) ---- changeFoo :: (Member (State Foo) r) => Eff r () changeFoo = modify $ \foo -> foo { foostr = "NYAN" } changeFooBar :: (Member (State Foo) r, Member (State Bar) r) => Eff r () changeFooBar = do changeFoo modify $ \bar -> bar { barint = 100 } main = do let foo = Foo { foostr = "hoge", fooint = 1 } let bar = Bar { barstr = "piyo", barint = 2 } print (foo, bar) print . run $ execState foo changeFoo print . run $ runState foo (execState bar changeFooBar)
モナド変換子のlift地獄解決のために作られたみたいなライブラリです。 あるモナドアクションが合成されたどのモナドのものなのかは、そのアクションの型で決定されるので、liftが不要になります。 ただし、型シグネチャがお伊達にも分かりやすいとは言えないほか、mtlとの互換性が全くないのはどうなんだろう・・・。
ブラックボックスの範囲の話をすると、いちおう圏論的なアプローチで作られてはいるようですが、数学的な背景がHask圏に収まっていないっぽくて、メタな部分で超絶技巧な事をしてたりします。 あとlensと組み合わせて使えるか今のところわかんないので、いずれやってみてまだブログ書きます。
Classy
{-# LANGUAGE TemplateHaskell #-} module Main where import Control.Lens import Control.Monad.State data Foo = Foo { _foostr :: String , _fooint :: Int } deriving Show data Bar = Bar { _barstr :: String , _barint :: Int } deriving Show makeClassy ''Foo makeClassy ''Bar ---- data FooBar = FooBar { _foobarFoo :: Foo , _foobarBar :: Bar } deriving Show makeLenses ''FooBar instance HasFoo FooBar where foo = foobarFoo instance HasBar FooBar where bar = foobarBar ---- changeFoo :: HasFoo a => State a () changeFoo = foostr .= "NYAN" changeFooBar :: (HasFoo a, HasBar a) => State a () changeFooBar = do changeFoo barint .= 100 main = do let foo = Foo { _foostr = "hoge", _fooint = 1 } let foobar = FooBar { _foobarFoo = foo , _foobarBar = Bar { _barstr = "piyo", _barint = 2 } } print foobar print $ execState changeFoo foo print $ execState changeFooBar foobar
モナド変換子とEffが「モナドの合成」なのに対し、こっちは「型の合成」になります。一旦型を宣言してしまえば、純粋な関数でもlensを通して使えます。 「型を作る」というアプローチであるが故に、型宣言の量が多くなってしまうのは難点ですが、その分、実際の処理部分は一番わかりやすいです。
ようは、「単純に複数の状態を一気に扱いたいだけなら、モナドの合成なんてややこしい事しないで、型を一つにまとめちゃえば良いでしょ?」という形での代案なので、List/Maybeモナドを代表とする、非決定計算を扱うモナドの代用は出来ないです。 Effとlensの組み合わせが出来るようであれば、Effと一緒に使ってみても良いかもしれないですね。