それらのほとんどすべて (ループおよび に関連する問題までmfix
) ではありませんCont
。
State
モナドを考える
newtype State s a = State (s -> (a,s))
フリーモナドのようには見えません...しかし、それをどのように使用するかState
という観点から考えてください
get :: m s --or equivalently (s -> m a) -> m a
set :: s -> m () --or (s,m a) -> m a
runState :: m a -> s -> (a,s)
操作をコンストラクターとしてリストすることにより、このインターフェースで自由なモナドを設計できます。
data StateF s a
= Get (s -> a) | Set s a deriving Functor
それから私たちは持っています
type State s a = Free (StateF s) a
と
get = Impure (Get Pure)
set x = Impure (Set x (Pure ())
そして、それを使用する方法が必要です
runState (Pure a) s = (a,s)
runState (Impure (Get f)) s = runState (f s) s
runState (Impure (Set s next)) _ = runState next s
ほとんどのモナドでこの構築を行うことができます。Maybe/partiality モナドが定義されているように
stop :: m a
maybe :: b -> (a -> b) -> m a -> b
ルールは、m x
一部で終わる関数のそれぞれをx
ファンクターのコンストラクターとして扱い、他の関数は結果のフリーモナドを実行する方法です。この場合
data StopF a = StopF deriving Functor
maybe _ f (Pure a) = f a
maybe b _ (Impure Stop) = b
なぜこれはクールなのですか?さて、いくつかのこと
- free モナドは、モナド コードの AST であると考えることができるデータの一部を提供します。このデータを操作する関数を書くことができます。これは DSL にとって非常に便利です。
- つまり、このようにモナドを分解すると、モナドは半構成可能になります。特に、代数を共有する 2 つの関手 ( が関手である場合、代数は本質的
f a -> a
に一部の関数にすぎません) が与えられた場合、構成もその代数を持ちます。 a
f
関手合成はただのことです いくつかの方法で関手を組み合わせることができますが、そのほとんどはその代数を維持します。この場合、ファンクターの合成では (f (g (x)))
なく、ファンクターの共積が必要です。ファンクタ追加
data f :+: g a = Inl (f a) | Inr (g a)
instance (Functor f, Functor g) => Functor (f :+: g) where
fmap f (Inl x) = Inl (fmap f x)
fmap f (Inr x) = Inr (fmap f x)
compAlg :: (f a -> a) -> (g a -> a) -> f :+: g a -> a
compAlg f _ (Inl x) = f x
compAlf _ g (Inr x) = g x
フリーモナドも代数を保存する
freeAlg :: (f a -> a) -> Free f a -> a
freeAlg _ (Pure a) = a
freeAlg f (Impure x) = f $ fmap (freeAlg f) x
Wouter Swierstra の有名な論文Data Types A La Carteでは、これが非常に効果的に使用されています。その論文の簡単な例は電卓です。この投稿の新しいモナドを取り上げます。与えられた代数
class Calculator f where
eval :: f Integer -> Integer
様々な事例が考えられます
data Mult a = Mult a a deriving Functor
instance Calculator Mult where
eval (Mult a b) = a*b
data Add a = Add a a deriving Functor
instance Calculator Add where
eval (Add a b) = a+b
data Neg a = Neg a deriving Functor
instance Calculator Neg where
eval (Neg a) = negate a
instance Calculator (Const Integer) where
eval (Const a) = a
data Signum a = Signum a deriving Functor
instance Calculator Signum where
eval (Signum a) = signum a
data Abs a = Abs a deriving Functor
instance Calculator Abs where
eval (Abs a) = abs a
そして最も重要な
instance (Calculator f, Calculator g) => Calculator (f :+: g) where
eval = compAlg eval
数値モナドを定義できます
newtype Numerical a = Numerical (
Free (Mult
:+: Add
:+: Neg
:+: Const Integer
:+: Signum
:+: Abs) a deriving (Functor, Monad)
そして、あなたは定義することができます
instance Num (Numerical a)
まったく役に立たないかもしれませんが、とてもクールだと思います。次のような他のものを定義できます
class Pretty f where
pretty :: f String -> String
instance Pretty Mult where
pretty (Mult a b) = a ++ "*" ++ b
残りのすべてについても同様です。
これは有用な設計戦略です: モナドに実行させたいことをリストします ==> 各操作のファンクターを定義します ==> その代数のいくつかがどうあるべきかを理解します ==> 各操作のファンクターを定義します ==> 作成します速い。
高速化は難しいですが、いくつかのトリックがあります。トリック 1 は、フリー モナドを (「高速化ボタン」で) ラップするCodensity
ことですが、それがうまくいかない場合は、フリー モナドを取り除きたいと考えます。私たちが持っていたときを思い出してください
runState (Pure a) s = (a,s)
runState (Impure (Get f)) s = runState (f s) s
runState (Impure (Set s next)) _ = runState next s
Free StateF a
これは、状態の定義が合理的であるように見えるため、結果の型を使用するための関数s -> (a,s)
です...しかし、操作をどのように定義するのでしょうか? この場合、あなたは答えを知っていますが、それを導き出す 1 つの方法は、Conal Elliott が型クラス射と呼ぶものの観点から考えることです。あなたがしたい
runState (return a) = return a
runState (x >>= f) = (runState x) >>= (runState f)
runState (set x) = set x
runState get = get
それはそれをかなり簡単にします
runState (return a) = (Pure a) = \s -> (a,s)
runState (set x)
= runState (Impure (Set x (Pure ())))
= \_ -> runState (Pure ()) x
= \_ -> (\s -> (a,s)) x
= \_ -> (a,x)
runState get
= runState (Impure (Get Pure))
= \s -> runState (Pure s) s
= \s -> (s,s)
これはかなり役に立ちます。この方法で導出>>=
するのは難しい場合があるため、ここには含めませんが、これらの他の定義はまさにあなたが期待する定義です。