在Adjoint functors determine monad transformers, but where's lift?中,西蒙C向我们展示了构造...
newtype Three u f m a = Three { getThree :: u (m (f a)) }
...,作为那里讨论的答案,可以给予instance Adjunction f u => MonadTrans (Three u f)
(附加语将其提供为AdjointT
)。因此,任何Hask / Hask附加都将导致monad变压器。尤其是StateT s
就是通过(,) s
和(->) s
之间的临时连接而产生的。
我的后续问题是:此构造是否可以推广到其他monad变压器?是否有一种方法可以从 transformers 程序包中从合适的附件中派生出其他变压器?
元备注:我的回答最初是为西蒙·C的问题写的。我选择将其分解为一个自我回答的问题,因为在重读该问题时,我注意到我所谓的回答与该评论中的讨论有关,而与问题本身无关。此问与答也可以跟进的另外两个紧密相关的问题是Is there a monad that doesn't have a corresponding monad transformer (except IO)?和Is the composition of an arbitrary monad with a traversable always a monad?
答案 0 :(得分:14)
此答案中的三种构造也可以以this Gist的形式复制。
newtype Three u f m a = Three { getThree :: u (m (f a)) }
...依靠f
和u
作为Hask内在伴随者。在StateT
的情况下可以解决此问题,但要使其更笼统,我们必须处理两个相关的问题:
首先,我们需要为变压器将要构建的“功能单子”找到合适的附加条件;和
第二,如果这样的附加条件使我们远离Hask,我们将不得不以某种方式解决无法直接使用Hask monad m
的事实。
我们可以尝试使用许多有趣的附件。特别是,每个单核都有两种附加语:Kleisli附加语和Eilenberg-Moore附加语(有关它们的详细分类,请参见Emily Riehl,Category Theory In Context,第5.2节)。在占该答案前半部分的分类偏移中,我将重点介绍Kleisli附加语,只是因为在伪Haskell中摆动比较容易。
(通过伪Haskell,我的意思是在后面的内容中会普遍使用符号。为了使您更容易理解,我将使用一些特殊的约定:|->
表示存在于事物之间的映射。不一定是类型; :
表示类似于类型签名的内容; ~>
表示非Hask射影;大括号和尖括号突出显示了选定的非Hask类别中的对象; .
也表示函子组成; F -| U
表示F
和U
是伴随函子。)
如果g
是Hask Monad
,则FK g -| UK g
之间有一个对应的Kleisli附加语FK g
,这使我们进入g
的Kleisli类别...
-- Object and morphism mappings.
FK g : a |-> {a}
f : a -> b |-> return . f : {a} ~> {b} ~ a -> g b
-- Identity and composition in Kleisli t are return and (<=<)
...和UK g
,使我们回到Hask:
UK g : {a} |-> g a
f : {a} -> {b} |-> join . fmap f : g a -> g b -- that is, (>>= f)
-- The adjunction isomorphism:
kla : (FK g a ~> {b}) -> (a -> UK g {b})
kra : (a -> UK g {b}) -> (FK g a ~> {b})
-- kla and kra mirror leftAdjunct and rightAdjunct from Data.Functor.Adjunction.
-- The underlying Haskell type is a -> g b on both sides, so we can simply have:
kla = id
kra = id
沿着Simon C的Three
行,让g
作为特征单子,将在其上构建变压器。遵循常规的Haskell术语,转换器将以某种方式合并另一个Hask monad m
的效果,我有时将其称为“基本monad”。
如果我们尝试在m
和FK g
之间挤压UK g
,则会遇到上述第二个问题:我们需要一个Kleisli-g
endofunctor,而不是一个Hask除了弥补之外,别无他法。就是说,我的意思是我们可以为函子定义函子(更确切地说,是两类endofunctors之间的函子),这有望将m
变成我们可以使用的函子。我将这个“更高”的函子称为HK g
。将其应用于m
应该会给出以下信息:
-- Keep in mind this is a Kleisli-g endofunctor.
HK g m : {a} |-> {m a}
f : {a} ~> {b} |-> kmap f : {m a} ~> {m b} ~ m a -> g (m b)
-- This is the object mapping, taking functors to functors.
-- The morphism mapping maps natural transformations, a la Control.Monad.Morph:
t : ∀x. m x -> n x |-> kmorph t : ∀x. {m x} ~> {n x} ~ ∀x. m x -> g (n x)
-- I won't use it explicitly, but it is there if you look for it.
(注意:漫长的分类乱拨。如果您急忙,请随意浏览“摘要”小节。)
UK g . HK g m . FK g
将是Hask endofunctor,Three
构造的对应物。我们进一步希望它成为Hask上的单子。我们可以通过将HK g m
设置为Kleisli-g
类别中的单子来确保这一点。这意味着我们需要找出Kleisli-fmap
上return
,join
和g
的对应对象:
kmap : {a} ~> {b} |-> {m a} ~> {m b}
(a -> g b) -> m a -> g (m b)
kreturn : {a} ~> {m a}
a -> g (m a)
kjoin : {m (m a)} ~> {m a}
m (m a) -> g (m a)
对于kreturn
和kjoin
,让我们尝试可能可行的最简单方法:
kreturn :: (Monad g, Monad m) => a -> g (m a)
kreturn = return . return
kjoin :: (Monad g, Monad m) => m (m a) -> g (m a)
kjoin = return . join
kmap
有点棘手。 fmap @m
会发出m (g a)
而不是g (m a)
,因此我们需要一种将g
层拉到外面的方法。碰巧有一种简便的方法可以做到这一点,但是它要求g
为a Distributive
functor:
kmap :: (Monad g, Distributive g, Monad m) => (a -> g b) -> m a -> g (m b)
kmap f = distribute . fmap f -- kmap = collect
当然,这些猜测毫无意义,除非我们可以证明它们是合法的:
-- Functor laws for kmap
kmap return = return
kmap g <=< kmap f = kmap (g <=< f)
-- Naturality of kreturn
kmap f <=< kreturn = kreturn <=< f
-- Naturality of kjoin
kjoin <=< kmap (kmap f) = kmap f <=< kjoin
-- Monad laws
kjoin <=< kreturn = return
kjoin <=< kmap kreturn = return
kjoin <=< kmap kjoin = kjoin <=< kjoin
计算得出composing monads with a distributive law的四个条件足以确保法律成立:
-- dist :: t (g a) -> g (t a)
-- I'm using `dist` instead of `distribute` and `t` instead of `m` here for the
-- sake of notation neutrality.
dist . fmap (return @g) = return @g -- #1
dist . return @t = fmap (return @t) -- #2
dist . fmap (join @g) = join @g . fmap dist . dist -- #3
dist . join @t = fmap (join @t) . dist . fmap dist -- #4
-- In a nutshell: dist must preserve join and return for both monads.
在我们的案例中,条件#1赋予kmap
身份,kjoin
权利身份和kjoin
关联性; #2具有kreturn
的自然度; #3,函子组成; #4,自然kjoin
(kjoin
的左身份不取决于这四个条件中的任何一个)。最终的健全性检查正在确定条件本身要满足的条件。在distribute
的特定情况下,其非常强的自然属性意味着任何合法Distributive
都必须具备四个条件,所以我们很高兴。
整个UK g . HK g m . FK g
单子可以通过将HK g m
拆分为Kleisli附加词而从我们已经拥有的片段中获得,这与我们开始时使用的Kleisli附加词完全相似,只是我们从{{ 1}}-g而不是Hask:
Klesili
现在我们手头有两个附加语,我们可以将它们组合起来,形成互感器附加语,最后将其转化为HK g m = UHK g m . FHK g m
FHK g m : {a} |-> <{a}>
f : {a} ~> {b} |-> fmap return . f : <{a}> ~> <{b}> ~ a -> g (m b)
-- kreturn <=< f = fmap (return @m) . f
-- Note that m goes on the inside, so that we end up with a morphism in Kleisli g.
UHK g m : <{a}> |-> {m a}
f : <{a}> ~> <{b}> |-> fmap join . distribute . fmap f : {m a} ~> {m b} ~ m a -> g (m b)
-- kjoin <=< kmap f = fmap (join @m) . distribute . fmap f
-- The adjunction isomorphism:
hkla : (FHK g m {a} ~> <{b}>) -> ({a} ~> UHK g m <{b}>)
hkra : ({a} ~> UHK g m <{b}>) -> (FHK g m {a} ~> <{b}>)
-- Just like before, we have:
hkla = id
hkra = id
-- And, for the sake of completeness, a Kleisli composition operator:
-- g <~< f = kjoin <=< kmap g <=< f
(<~<) :: (Monad g, Distributive g, Monad m)
=> (b -> g (m c)) -> (a -> g (m b)) -> (a -> g (m c))
g <~< f = fmap join . join . fmap (distribute . fmap g) . f
和return
:
join
(有关单元和辅基组成的分类解释,请参见Emily Riehl,语境中的类别理论,命题4.4.4。)
对于-- The composition of the three morphism mappings in UK g . HK g m . FK g
-- tkmap f = join . fmap (kjoin <=< kmap (kreturn <=< return . f))
tkmap :: (Monad g, Distributive g, Monad m) => (a -> b) -> g (m a) -> g (m b)
tkmap = fmap . fmap
-- Composition of two adjunction units, suitably lifted through the functors.
-- tkreturn = join . fmap (hkla hkid) . kla kid = join . fmap kreturn . return
tkreturn :: (Monad g, Monad m) => a -> g (m a)
tkreturn = return . return
-- Composition of the adjunction counits, suitably lifted through the functors.
-- tkjoin = join . fmap (kjoin <=< kmap (hkra kid <~< (kreturn <=< kra id)))
-- = join . fmap (kjoin <=< kmap (return <~< (kreturn <=< id)))
tkjoin :: (Monad g, Distributive g, Monad m) => g (m (g (m a))) -> g (m a)
tkjoin = fmap join . join . fmap distribute
,lift
听起来很明智。总计为kmap (return @g)
(与Benjamin Hodgson's answer to Simon C's question中的distribute . fmap return
相比),根据条件#1可以简单地变成:
lift
tklift :: m a -> g (m a)
tklift = return
定律(意味着MonadLift
必须是monad态)确实成立,tklift
定律依赖于分布条件#1:
join
Kleisli附加词使我们可以通过将tklift . return = tkreturn
tklift . join = tkjoin . tkmap tklift . tklift
单子构成在任何其他单子的外部来构造一个transfomer。放在一起,我们有:
Distributive
-- This is still a Three, even though we only see two Hask endofunctors.
-- Or should we call it FourK?
newtype ThreeK g m a = ThreeK { runThreeK :: g (m a) }
instance (Functor g, Functor m) => Functor (ThreeK g m) where
fmap f (ThreeK m) = ThreeK $ fmap (fmap f) m
instance (Monad g, Distributive g, Monad m) => Monad (ThreeK g m) where
return a = ThreeK $ return (return a)
m >>= f = ThreeK $ fmap join . join . fmap distribute
$ runThreeK $ fmap (runThreeK . f) m
instance (Monad g, Distributive g, Monad m) => Applicative (ThreeK g m) where
pure = return
(<*>) = ap
instance (Monad g, Distributive g) => MonadTrans (ThreeK g) where
lift = ThreeK . return
的典型示例是函数函子。将其放在另一个monad的外面给出Distributive
:
ReaderT
newtype KReaderT r m a = KReaderT { runKReaderT :: r -> m a }
deriving (Functor, Applicative, Monad) via ThreeK ((->) r) m
deriving MonadTrans via ThreeK ((->) r)
实例与规范的ThreeK
实例完全一致。
在上面的推导中,我们将基本monad Klesli附加语堆叠在特征monad附加语之上。可以想象,我们可以反过来做,从基本monad附加函数开始。当定义ReaderT
时,将会发生的关键变化。由于基本monad原则上可以是任何monad,因此我们不希望对它施加一个kmap
约束,以便可以向外拉它,就像在上面的派生中对Distributive
所做的那样。更好地适合我们的游戏计划的是,双重要求功能monad提供g
,以便可以与Traversable
一起推入。这将导致一个在内部而不是外部构成特征单子的变压器。
下面是整体内部结构。之所以称其为sequenceA
,是因为也可以通过使用Eilenberg-Moore附加语(而不是Kleisli附加语)并将它们与基本monad堆叠在一起来获得,就像Simon C的ThreeEM
一样。这个事实可能与Eilenberg-Moore附加物和Klesili附加物之间的对偶有关。
Three
以这种方式出现的普通变压器包括newtype ThreeEM t m a = ThreeEM { runThreeEM :: m (t a) }
instance (Functor t, Functor m) => Functor (ThreeEM t m) where
fmap f (ThreeEM m) = ThreeEM $ fmap (fmap f) m
instance (Monad t, Traversable t, Monad m) => Monad (ThreeEM t m) where
return a = ThreeEM $ return (return a)
m >>= f = ThreeEM $ fmap join . join . fmap sequenceA
$ runThreeEM $ fmap (runThreeEM . f) m
instance (Monad t, Traversable t, Monad m) => Applicative (ThreeEM t m) where
pure = return
(<*>) = ap
-- In terms of of the Kleisli construction: as the bottom adjunction is now the
-- base monad one, we can use plain old fmap @m instead of kmap to promote return.
instance (Monad t, Traversable t) => MonadTrans (ThreeEM t) where
lift = ThreeEM . fmap return
和MaybeT
。
这种构造有一个潜在的陷阱。 ExceptT
必须遵守分配条件,以使实例合法。但是,它的sequenceA
约束使其自然性比Applicative
弱得多,因此条件并非全部免费:
条件#1确实成立:这是identity and naturality laws of Traversable
的结果。
条件2也成立:distribute
保留可遍历函子上的自然变换,只要这些变换保留sequenceA
。如果我们将toList
视为对return
的自然转变,那将立即成立案例。
条件#3。如果将Identity
作为对join @m
的自然转换,保留了Compose m m
,那将成立,但是事实并非如此。如果(<*>)
实际上对效果进行排序(也就是说,如果可遍历可以包含多个值),则由sequenceA
和join
在基本monad中执行的顺序引起的任何差异将导致违反条件。顺便说一句,这是臭名昭著的“ ListT做错了”问题的一部分:按照这种构造构建的变压器中的(<*>)
仅在与可交换基本单子一起使用时才合法。
最后,条件#4仅在ListT
作为对join @t
的自然转换而保留Compose t t
的情况下成立(也就是说,如果它不丢失,则重复,或重新排列元素)。结果是,这种构造不适用于其toList
“采用嵌套结构的对角线”的要素monad(通常也是join
实例的monad),即使我们尝试通过限制自己只能使用可交换的基本单子来满足条件#3。
这些限制意味着该结构不能像人们希望的那样广泛地应用。最终,Distributive
约束太宽泛了。我们真正需要的可能是使monad功能具有仿射可遍历的能力(也就是说,一个容器最多容纳一个元素-有关某些镜头风格的讨论,请参见this post by Oleg Grenrus);据我所知,还没有规范的Haskell类。
到目前为止,所描述的构造要求特征monad分别为Traversable
或Distributive
。但是,总体策略并非完全针对Kleisli和Eilenberg-Moore附加语,因此可以想象在使用其他附加语时尝试使用它。尽管Traversable
既不是StateT
也不是Three
,但是通过临时附加语会通过Simon C的AdjointT
/ State
导致Distributive
说明这样的尝试可能会富有成果。但是,我对此并不乐观。
In a related discussion elsewhere,本杰明·霍奇森(Benjamin Hodgson)猜想,所有诱导monad的附加语都指向同一个变压器。考虑到所有这些附加语都通过独特的函子与Kleisli和Eilenberg-Moore附加语相关联,这听起来很合理(关于此,请参见上下文中的“类别理论” ,命题5.2.12)。恰当的例子:如果我们尝试使用Traversable
构造List
,但使用自由/健忘的附加物类别而不是Kleisli-ThreeK
,我们将以{{1 }}转换[]
/ in-the-in-side构造会给我们,直到需要m []
成为适用同态的“ ListT做错了问题”。
那么ThreeEM
及其产生变压器的第三附加呢?尽管我尚未详细说明,但我认为将这里构造中使用的join
和State
分别属于左右伴随关系更合适,而不是整个功能单子。对于distribute
,这将超出Haskell类型签名的范围……
sequenceA
...了解Kleisli-distribute
-Hask仿函数之间的自然转换:
distribute :: (Distribute g, Functor m) => m (g a) -> g (m a)
如果我对此表示正确,则有可能将这个答案转过来,并根据特征monad的Kleisli附加词重新解释g
/ distribute : m . UK g |-> UK g . HK g m
的构造。在这种情况下,Three
根本不会告诉我们其他既不是AdjointT
也不是State
的功能单子。
还值得注意的是,并非所有的变压器都是通过按这里所见的方式通过附加成分的组合来混合单声道效果而产生的。在变形金刚中,Distributive
和[Traversable
不遵循该模式;但是,我会说它们太古怪而无法在这种情况下进行讨论(“不是单子类的函子”,as the docs point out)。各种"ListT done right"实现方案提供了一个更好的示例,这些实现方案通过与基本ContT
结合的基本monad效果避免了与SelectT
相关的非法问题(以及流问题的丢失),需要在变压器的绑定中对它们进行排序。这是一个实现的草图,出于说明目的:
sequenceA
在此-- A recursion-schemes style base functor for lists.
data ListF a b = Nil | Cons a b
deriving (Eq, Ord, Show, Functor)
-- A list type might be recovered by recursively filling the functorial
-- position in ListF.
newtype DemoList a = DemoList { getDemoList :: ListF a (DemoList a) }
-- To get the transformer, we compose the base monad on the outside of ListF.
newtype ListT m a = ListT { runListT :: m (ListF a (ListT m a)) }
deriving (Functor, Applicative, Alternative) via WrappedMonad (ListT m)
-- Appending through the monadic layers. Note that mplus only runs the effect
-- of the first ListF layer; everything eslse can be consumed lazily.
instance Monad m => MonadPlus (ListT m) where
mzero = ListT $ return Nil
u `mplus` v = ListT $ runListT u >>= \case
Nil -> runListT v
Cons a u' -> return (Cons a (u' `mplus` v))
-- The effects are kept apart, and can be consumed as they are needed.
instance Monad m => Monad (ListT m) where
return a = ListT $ pure (Cons a mzero)
u >>= f = ListT $ runListT u >>= \case
Nil -> return Nil
Cons a v -> runListT $ f a `mplus` (v >>= f)
instance MonadTrans ListT where
lift m = ListT $ (\a -> Cons a mzero) <$> m
中,基本单声道效果既不在列表的内部也不在列表的外部。相反,它们用螺栓固定在列表的书脊上,通过根据ListT
定义类型使它们变得有形。
以类似方式构建的相关转换器包括free-monad转换器FreeT
,以及有效流媒体库中的核心monad转换器(我包括的“ ListT做得正确”链接并非偶然)以上指向管道文档)。
这种变压器是否可以与此处描述的附加堆叠策略相关?我还没有足够努力地解决这个问题。这似乎是一个值得思考的有趣问题。