読者です 読者をやめる 読者になる 読者になる

Creatable a => a -> IO b

Haskellと数学とちょびっと音楽

状態の合成比較:モナド変換子 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と一緒に使ってみても良いかもしれないですね。