モチベーション: ListT done right を FreeT から作る
ListT done right (ちゃんとした方のListT) というモナド変換子をご存知でしょうか?
ListT
(done wrong) はこのブログで度々取り上げました(2020年, 2021年)
が、done right はこれとは別物です。(done wrong)はほぼ使われることはない1のですが、done right は結構便利なモナドです。
さて、本記事では ListT done right にしか用がないので、それをListT
とだけ呼ぶことにします。
ListT m a
は、要素a
を一つ取り出すたびにモナドm
の副作用が必要になるようなリストを表している、と考えることができます。
例えば、ネットワーク越しの通信によってデータを少しづつ読み出すときには ListT IO ByteString
が使えます。
ListT
にはlist-t, list-transformer, List
など複数の実装があります2。初めてこのモナドに触れる方にはlist-transformerのドキュメントが最もわかりやすいかと思います。
list-transformer のListT
の定義を以下に引用します。
newtype ListT m a = ListT { next :: m (Step m a) }
data Step m a = Cons a (ListT m a) | Nil
さて、このListT
ですが、モナドの演算を忘れて単なる型として見れば、
FreeT
モナド変換子(Control.Monad.Trans.Free)の特殊ケースになっています。
FreeT
は以下のように定義されています。
newtype FreeT f m a = FreeT { runFreeT :: m (FreeF f a (FreeT f m a)) }
data FreeT f a r = Pure a | Free (f r)
このとき、次のような同型があります3。
ListT m a <-> FreeT ((,) a) m ()
ここで、((,) a)
はタプル型のコンストラクタ(,)
にa
を部分適用した関手、つまり((,) a) b = (a,b)
のことです。
読みやすさのために、Pair = (,)
という別名を付けて書くことにします。
type Pair = (,)
ListT m a <-> FreeT (Pair a) m ()
さて、モナドm
を固定したとき、FreeT f m
は、適当なFunctor
であるf
から別のFunctor
であるFreeT f m
を構成していると見なすことができます。
そのような性質の型コンストラクタmm
、すなわちFunctor f
に対してmm f
もFunctor
であるようなmm
について、
mm (Pair a) ()
がMonad
として振る舞うような一般化、つまり
GeneralizedListT mm a <-> mm (Pair a) ()
として、GeneralizedListT mm
がMonad
になるよう、FreeT
を一般化できないか、と考えると、
FFunctor
とFMonad
という抽象化にたどり着きます。
Trailモナド: FMonadから作ったMonad
以前、Functor
上のモナドFMonad
というものを紹介しました。
上記記事の内容をHaskellのライブラリとして整理したものがGitHubにあります。本記事ではしばしば このリポジトリ上のソースコードへのリンクを張ります。
- 見ればわかるように未完成(本記事投稿時点でのHEAD)ですし、Hackageには出していません
FFunctor
とFMonad
についておさらいします。FFunctorは、
Functor
における関数の型 a -> b
を自然変換 f ~> g = (∀x. f x -> g x)
に置き換えたようなクラスです。
つまり、Functor f
が a -> b
を f a -> f b
に写すように、 FFunctor ff
は g ~> h
を ff g ~> ff h
に写します。
type f ~> g = ∀x. f x -> g x
class (forall g. Functor g => Functor (ff g)) => FFunctor ff where
ffmap :: (Functor g, Functor h) => (g ~> h) -> ff g x -> ff h x
-- :: (Functor g, Functor h) => (g ~> h) -> (ff g ~> ff h)
FMonadは、同様に、Monad
に対して関数を自然変換に置き換えたクラスです。
class FFunctor ff => FMonad ff where
fpure :: Functor g => g ~> ff g
fjoin :: Functor g => ff (ff g) ~> ff g
あるff
がFMonad
だからといって、ff g
がMonad
になったりはしないことに注意してください。
以下の2組の操作はまったく別のことを意味しています。
-- FMonad ff のときできる操作
fpure :: g a -> ff g a
fjoin :: ff (ff g) a -> ff g a
-- Monad (ff g) のときできる操作
pure :: a -> ff g a
join :: ff g (ff g a) -> ff g a
しかし、FMonad
からMonad
を新たに作りだす方法があります。その方法の一つがTrail
モナドです。
(Control.Monad.Trail参照)
-- | Trail は FMonad をとって Monad を作る
type Trail :: ((Type -> Type) -> Type -> Type) -> Type -> Type
-- -- ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^
-- FMonadのカインド |
-- Monadのカインド
newtype Trail mm a = Trail { runTrail :: mm (Pair a) () }
これを行う既存の概念はなさそうなので、勝手に命名しちゃいました。“trail”というのは野山を通る小道のことで、
舗装されていない踏み跡が道になったようなイメージです。けもの道が”animal trail”ですね。
このMonad
をtrailと名付けた理由は、ListT
の一般化である、という所からです。ListT m a
は、
m ()
の計算過程にa
型のデータを足跡のようにつけて行ったものというイメージをしていたので、そこから取っています。
まず、Trail mm
がFunctor
であることを見てみます。Trail mm a
は mm (Pair a) ()
をラップした型なので、
関数f :: a -> b
に対してmm (Pair a) () -> mm (Pair b) ()
という関数を作れればfmap
になります。
instance (FFunctor mm) => Functor (Trail mm) where
fmap f = Trail . ffmap (first f) . runTrail
ffmap (first f)
は所望の型を持っていますね! first f
で、
関数f :: a -> b
を自然変換∀c. Pair a c -> Pair b c
として表してくれたおかげで、
自然変換をとるffmap
に渡せることがポイントです。
f :: a -> b
f :: ∀c. Pair a c -> Pair b c
first f :: (Pair a) ~> (Pair b)
first :: mm (Pair a) ~> mm (Pair b) ffmap (first f)
FMonad mm
が与えられたとき、Trail mm
がMonad
になる理由も見てみましょう。
pureTrail :: FMonad mm => a -> Trail mm a
= Trail $ fpure (a,())
pureTrail a
joinTrail :: FMonad mm => Trail mm (Trail mm a) -> Trail mm a
= Trail . fjoin . ffmap (plug . first runTrail) . runTrail
joinTrail
plug :: forall f x. Functor f => Pair (f ()) x -> f x
= a <$ f_
plug (f_,a)
instance FMonad mm => Applicative (Trail mm) where
pure = pureTrail
<*>) = ap
(
instance FMonad mm => Monad (Trail mm) where
>>= k = joinTrail (fmap k ma) ma
pureTrail
の「型が合っている」様子は次の通りです。
:: Pair a ()
(a, ()) :: mm (Pair a) ()
fpure (a, ()) a :: Trail mm a pureTrail
joinTrail
は結構複雑です。1ステップずつ追っていくと、次のようになっています。
runTrail :: Trail mm a -> mm (Pair a) ()
runTrail :: Pair (Trail mm a) ~> Pair (mm (Pair a) ())
firstplug :: Pair (f ()) ~> f
. first runTrail :: Pair (Trail mm a) ~> mm (Pair a)
plug
let pf = plug . first runTrail -- 省略
pf :: Pair (Trail mm a) ~> mm (Pair a)
pf :: mm (Pair (Trail mm a)) ~> mm (mm (Pair a))
ffmapfjoin :: mm (mm f) ~> mm f
. ffmap pf :: mm (Pair (Trail mm a)) ~> mm (Pair a)
fjoin . ffmap pf :: mm (Pair (Trail mm a)) () -> mm (Pair a) ()
fjoin joinTrail :: Trail mm (Trail mm a) -> Trail mm a
この定義がMonad
則を満たしている証明はここでは省略します。
等式を並べただけの荒いものですが、
証明はソースコード中にコメントとして残してあります。
ListT を FreeT から作れるか
さて、FMonad
はListT
をFreeT
として表すことの一般化を狙っていた、と言いました。
実際にこれは達成できています。
FreeT f m a
の引数f
とm
を入れ替えた型をFreeT'
と定義します。
import qualified Control.Monad.Trans.Free as Original
newtype FreeT' m f b = WrapFreeT' { unwrapFreeT' :: Original.FreeT f m b }
m
がMonad
ならばFreeT' m
はFMonad
になります。
instance Monad m => FMonad (FreeT' m)
したがって、Trail (FreeT' m) a
もMonad
になりますが、newtype
による読み替えを外していけば
Trail (FreeT' m) a <-> FreeT' m (Pair a) ()
<-> FreeT (Pair a) m ()
となり、これはListT m a
と同型なのでした。そして期待通りに、Monad
としても同じになるよう定義できています4。
Trail は ListT 以外にどんな Monad を作れるの?
FreeT'
以外の FMonad mm
を入れてみて、 Trail mm
がどのようなモナドになったか調べてみましょう。
Free (Freeモナド)
Trail Free
はリストモナド[]
になります。data Free f x = Pure x | Free (f (Free f x)) Trail Free a <-> Free (Pair a) () <-> Either () (Pair a (Free (Pair a) ())) <-> Maybe (a, Trail Free a)
Ap (Free Applicative)
Trail Ap
もリストモナド[]
になります。data Ap f a where Pure :: a -> Ap f a Ap :: f a -> Ap f (a -> b) -> Ap f b
疑似コードですが、上の定義で
Ap f ()
の場合を考えると、とても単純な形になることがわかります。data Ap f () where Pure :: () -> Ap f () Ap :: f a -> Ap f (a -> ()) -> Ap f ()
ここで
a -> ()
は()
と同型、存在量化された型変数a
は何も使いみちがないため任意の型を入れてよく、a = ()
を代入できます。するとdata Ap f () where Pure :: Ap f () Ap :: f () -> Ap f () -> Ap f ()
これは
[f ()]
と完全に同じです。したがって、Trail Ap a <-> Ap (Pair a) () <-> [Pair a ()] <-> [a]
となります。
-
Trail (Compose m)
はモナドとしてはただのm
になります。newtype Compose f g x = Compose { getCompose :: f (g x) } Trail (Compose m) a <-> Compose m (Pair a) () <-> m (Pair a ()) = m (a, ()) <-> m a
-
Trail (ComposePre m) a
はモナドとしてはWriter (m ())
になります。 ここでm ()
は(>>) :: m () -> m () -> m ()
によってMonoid
になっています。newtype ComposePre g f x = ComposePre { getComposePre :: f (g x) } Trail (ComposePre m) a <-> ComposePre m (Pair a) () <-> Pair a (m ()) = (a, m ())
この辺りはよく見知ったMonad
しか出てきませんね。ですが複雑であまり見たことがないものも現れます。
ApT (Free Applicative Transformer5)
data ApT f g x = PureT (g x) | forall a b c. ApT (g a) (f b) (ApT f g c) (a -> b -> c -> x)
ApT
は2通りの方法でFMonad
になります。 1つ目はApT f g
でf
を固定してg
を引数と見たとき、もう一つはg
を固定してf
を引数と見たときです。instance Functor f => FMonad (ApT f) instance Applicative g => FMonad (Flip1 ApT g) newtype Flip1 t g f x = Flip1 { unFlip1 :: t g f x }
ApT
の定義はやや複雑ですが、Ap
のときと同様、x = ()
に限って考えると単純化できます。data ApT f g () = PureT (g ()) | ApT (g ()) (f ()) (ApT f g ())
これは
g ()
とf ()
が交互に並んだリストになっています。つまり、以下のように定義されるAltList
型を使って、ApT f g ()
はAltList (f ()) (g ())
と同型になっています。-- [b,a,b,a,...,b] と交互に並んだリスト data AltList a b = Last b | Next b a (AltList a b) -- 実際のHaskellではできませんが、ここでは、`AltList a b`をリストの記法で以下のように -- 書いてよいことにします。 :: AltList a b [b] :: AltList a b [b, a, b] :: AltList a b [b, a, b, a, b]-- ...
Trail (ApT f) b
は次のように「b
とf ()
とが交互に並んだリスト」と同型です。..., b] [b, f (), b, f (), b, f (),
Monad
としては、Monad (AltList a)
というインスタンスにa = f ()
とした場合になっています。 このMonad
がどのようなものか例を挙げると、pure b = [b] join [[b], a, [b,a,b,a,b], a, [b,a,b]]= [ b, a, b,a,b,a,b, a, b,a,b ]
のように、ネストした
AltList a (AltList a b)
を平坦にする操作をjoin
とするMonad
です。もう一方の
Trail (Flip1 ApT g) a
は、g ()
とa
が交互に並んだリストです。..., g ()] [g (), a, g (), a,
Monad
としては、AltList
の引数を入れ替えたnewtype AltList' b a = AltList' (AltList a b)
が持つ、
Monoid b => Monad (AltList' b)
というインスタンスがベースになります。 このモナドは次のような演算を持ちます。pure a = [mempty, a, mempty] join [b, [b], b, [b, a, b, a, b], b]= [b <> b <> b <> b, a, b, a, b <> b]
Trail (Filp1 ApT g)
は、g ()
をApplicative
を使ってMonoid
とみなし、AltList' (g ())
と同型になります。
さて、FreeT
を使ってListT
を作り出したとき、FreeT f m
のm
を固定してf
を引数とみなしました。
実は、f
を固定してm
を引数とみなしても、FreeT
はFMonad
になります。
instance Functor f => FMonad (FreeT f)
Trail (FreeT f) a
はどのようなMonad
でしょうか?t a = Trail (TreeT f) a
と置いて定義を展開していくと次のようになります。
= Trail (FreeT f) a
t a <-> FreeT f (Pair a) ()
<-> Pair a (FreeF f () (FreeT f (Pair a) ()))
<-> Pair a (FreeF f () (t a))
<-> Pair a (Either () (f (t a)))
<-> Pair a (Maybe (f (t a)))
= (a, Maybe (f (t a)))
<-> (a, Maybe (f (t a))) t a
すなわち、以下のように定義された型がMonad
になって、それがTrail (FreeT f)
と同型になります。
data T f a = a :< Maybe (f (T f a))
このMonad
、私は全く見たことがなかったので6、T
という味気ない一文字の代わりにEfreet
という名前を勝手に付けてtwitterでやいのやいのしていました。
この型はComonadにもなっているので、ちょっと面白かったんですよね。
理論的には面白いモナドなんですが、実用する機会は無いと言っていいと思います。↩︎
どれも事実上同じモナドを実装しています。実装方法の違いによる性能差はあるかもしれませんが私は検証していません。パッケージ間の主な違いはモナドを使いやすくするAPIです。↩︎
Monadとしての同型ではなく、
ListT m
のモナド演算とFreeT f m
のモナド演算に直接の関係はありません。↩︎同じになっていることの証明はhandwaveさせて下さい。ごめんなさい・・・↩︎
freeパッケージにも”Free Applicative Transformer”と呼ばれているもの(Control.Applicative.Trans.Free) が含まれますが、それとは異なります。↩︎
Twitterで”ちょっと違うけど似ているMonadを使ったことがあるよ”と教えていただきました(twitter)。この違いがどの程度影響しているのか等、自分はまだよく解っていませんので、理解が進んだらまた紹介するかもしれません。↩︎