2008-02-28
オブジェクトをモナド変換子に書き換える (1)
- インスタンス変数のうち変化するものと変化しないものを書き出す
- それぞれをまとめてdataを作る
data ImmutableFields = IF { ... } deriving (Show) data MutableFields = MF { ... } deriving (Show)
- 以下のようにnewtypeを定義
- newtype deriving(id:illillli:20080217#p1)が必要
- TとrunTは公開しない
- 二回ラップしているからか、MonadTransはderivingがうまくいかない
newtype HogeT m a = T { runT :: ReaderT ImmutableFields (StateT MutableFields m) a } deriving ( Functor , Monad , MonadIO , MonadReader ImmutableFields , MonadState MutableFields ) instance MonadTrans HogeT where lift = T . lift . lift
- 他に必要なモナドがあったら、変換子としてその都度mに要求する (試してない)
hoge :: (MonadFuga m) => SomeClassT m a
- コンストラクタ代わりの関数を書く
- パラメータはちゃんとtypeしておくこと
- 元のnewにデフォルト引数があったら defaultHoge :: Hoge を作っておく
newHoge :: Count -> Name -> Hoge
- ランナーを書く(IO込みの初期化があったらここでやる)
runHogeT :: (Monad m) => Hoge -> HogeT m a -> m (a, Hoge)
- アクセサを書き直す
setSomeField = modify $ \fs -> fs { someField = x }
- メソッドを書き直す
- IOはliftIO $ do ...内で行う
- エクスポートするのは以下のものだけ
- HogeT ()
- type各種
- newHoge
- runHoge, withHoge
- アクセサ
- メソッド
ここまでで一息。変化しないフィールドを分けているのはバグが減りそうなため。
この時点ですでにプログラムは書けるが、もうひと手間かけるとliftがうるさいのを減らすことができる。
オブジェクトをモナド変換子に書き換える (2)
(1)が構造体と操作を書く作業にあたるなら、(2)は継承したりさせたりする作業にあたる。
上の手順で作ったコードは(クラス名).Lazyモジュールに置く。(mtlの流儀)
以下の手順で書くコードは(クラス名).Classに置く。
- 公開するアクセッサやメソッドを書き出す
- 必要なtype定義をClassモジュールに移す
- アクセッサとメソッドのexportをやめて、代わりにmodule Hoge.Classをエクスポートするようにする
- 以下のようにクラスを定義
- 要はLazyモジュールにある公開メソッドの変換子を縮めたもの
class (Monad m) => MonadHoge m where getHoge :: m Moga setHoge :: Piyo -> m () doFuga :: m ()
- State, Reader, Writer, Cont, Errorなどのインスタンスを気の済むまで定義
- 都合のためメソッドの型が多々書き換わるので注意 (それだけ計算の本質に近づく?)
instance (MonadHoge m) => MonadHoge (StateT s m) where getHoge = lift getHoge doFuga = ... (ここがパズルのようで結構めんどかったりする)
newtype FugaT m a = T { ... } deriving (MonadHoge)
- できるならLazy側でもインスタンスを定義
- こちらはMonadFixやMonadPlusなどをするのが普通?
- (MonadStateとMonadReaderは予約済みだけど)
instance (Monoid w, MonadWriter w m) => MonadWriter w (HogeT m) where ...
Lazyというモジュールに置いただけあって、Strict版も用意するべき。
オブジェクトをモナド変換子に書き換える (3)
Strict版も用意するべきなのだが、どうすればよいのだろう。
mtlのソースコードを見てもいまいち違いが分からなかった。ユーティリティ関数は全く同じ定義で、MonadやFunctorにインスタンス化しているところだけが違っていた。
instance Monad (State s) where return a = State $ \s -> (a, s) m >>= k = State $ \s -> let (a, s') = runState m s in runState (k a) s'
instance Monad (State s) where return a = State $ \s -> (a, s) m >>= k = State $ \s -> case runState m s of (a, s') -> runState (k a) s
letとcaseで動作が違うのだろうか。
仕様書を見ると、caseの方はマッチする値が_|_なら_|_で、letの方は値が_|_でも評価されるまでエラーにならないらしい。GHCのマニュアルの方にはっきり「letは遅延でcaseとパターンマッチは正格」と書いてあった。
単純に考えると、各フィールドのデータ型に!をつけて、Control.Monad.Stateの代わりにControl.Monad.State.Strictをインポートするだけでいいのではないかと思う。
オブジェクトをモナド変換子に書き換える (4)
こうして定義したモナド変換子は次のように使う。
import Hoge main = do let hoge = newHoge 1 2 3 (x, hoge') <- runHoge hoge test print (x, hoge') test :: (MonadIO m) => HogeT m () test = do f <- getFuga liftIO $ print f setFuga 5 f <- getFuga liftIO $ print f
オブジェクトをモナド変換子に書き換える (5)
いくつか問題がある。
- ReaderT (StateT m)かStateT (ReaderT m)か
- コンストラクタ周りが怪しげ
- IOしなくていいのか?
- 後で更新されてないものが扱える
- 複数のオブジェクトを同時に扱えない
runを複数回することになるし、結局外に出た時に更新された状態を受け取らないといけない。
Data.Array.MArrayなどを見ると、「このモナドの中ではこの型が使える」というやり方の方がいいのかもしれない。
- liftIOがうるさい
これは諦めるしかない。斜め読みしたlambdabotのコードでは、io = liftIOという行があった。
- 他の変換子とのインスタンス定義が怪しげ
MonadHoge側からインスタンスを定義するのが便利なのは確かだが、あまりやりすぎるとあとで書き足す時に「すでにインスタンスがあります」とエラーになりそうだ。一方HogeTを何かのインスタンスにするのはおそらく安全。
なにかルールを定める必要があるが、クラスや変換子の分類が必要そう。
試したのはRuby/SDLのfpstimer.rbだけなので、他のクラスも書き換えながら記事を書き足していくつもり。
疑問や似たようなことをしている例があれば、どなたか教えてください。