処理の切り替えとFreezeパターン
以下、Objectというとメソッドと適当な引数を与えると自分自身を返したり返さなかったりするデータ型、またはobjectiveのObjectということにしておきます。
newtype Object xs = Object { runObject :: forall a. Methods xs a -> ctx (Object xs) a }
さてオブジェクトを実行し続けて、あるタイミングで別の処理に切り替えたいということはままある。一番よくある例ではあるタイミングでオブジェクトが死ぬ、死んだらオブジェクトを破棄する処理に移行する、みたいなことがしたい、とか。
そのままオブジェクトを丸ごと破棄してGCに回収させればよいのであれば何も考えなくていいんだけれど、大抵の場合は後処理があり、後処理には死ぬ直前のオブジェクトからデータを拾ったり、ということはまぁあるよねと。
おそらく一番簡単なのは isDead
フラグをオブジェクトの内部状態として持たせておいて isDead
を毎回確認するというもので、これは意外と悪くないのだけれどフラグが必要なため内部状態が1つ増えるわけである。
管理する内部状態が増え、しかもこのようなものが全てのオブジェクトに生えることを許すわけにはいかないのでこれをやめたい。で、特定のタイミングで処理を切り替えたい場合には、次のようなFreezeパターンを使ってみるのがいいんじゃないかという提案。
data Freeze w a = Freeze w a | Keep w _Frozen :: Lens' (Freeze w a) w
名前の通り、普通の値は Keep
に入れて、死んだオブジェクトあるいは一旦処理を切りたい時に Freeze
に入れる。これを実際にオブジェクトを定義する時に使うときには例えば次のようにする。
newtype FreezeT w m a = FreezeT { runFreezeT :: m (Freeze w a) } op'switch :: Object xs -> FreezeT (Object xs) m a -- runAndKill :: Object xs -> (Object xs -> m r) -> (Object xs -> DeleteFlag -> m r) -> m r runAndKill obj update kill = do obj' <- op'run obj fw <- runFreezeT $ op'switch obj' case fw of Freeze o a -> kill o a Keep o -> update o where op'run :: Object xs -> m (Object xs)
例えばオブジェクトを一旦Freezeさせてから特定のタイミングで再初期化して復活させる、みたいなこともあると思うのでそういうこともやってみよう。
-- 初期化 op'initialize :: args -> m (Object xs) -- run -> switchを繰り返す -- switchは死んだかどうかのチェックに使う op'run :: Object xs -> m (Object xs) op'switch :: Object xs -> FreezeT (Object xs) m a -- 死んだobjectを復活させる op'reset :: Object xs -> a -> args -> Object xs -- 死んだobjectを破棄する op'delete :: Object xs -> a -> m () -- runObject args = do obj0 <- op'initialize args (obj,a) <- mainloop obj0 op'delete obj a where mainloop objN = do objN' <- op'run objN fw <- runFreezeT $ op'switch objN' case fw of Freeze w a | breakMainLoop -> return (w,a) Freeze w a -> mainloop $ op'reset w a args Keep w -> mainloop w
こういう感じで、「一旦止めて」「特定のタイミングで別の処理に切り替える」みたいなことがしたいときは便利なんじゃなかろうか。
最近思いついて使っているので共有。