应用组成,单子不组成

Applicatives compose, monads don't

提问人:missingfaktor 提问时间:8/12/2011 最后编辑:imz -- Ivan Zakharyaschevmissingfaktor 更新时间:5/19/2021 访问量:15639

问:

应用组成,单子不组成。

上述声明是什么意思?什么时候一个比另一个更可取?

Haskell 函数编程单 子变压器 应用

评论

6赞 fuz 8/12/2011
你从哪里得到这个声明?查看一些上下文可能会有所帮助。
0赞 missingfaktor 8/12/2011
@FUZxxl:我从许多不同的人那里反复听到它,最近来自 twitter 上的 debasishg。
3赞 C. A. McCann 8/13/2011
@stephen Tetley:请注意,许多这样的 s 实际上是一个完整的 s 家族,即每个可能的“形状”结构都有一个。 不是 ,而是固定长度的 s 是。 是一种方便的特殊(或者是一般的?)情况,其中“结构”的大小被固定为环境类型的基数。ApplicativeMonadZipListMonadZipListReader
3赞 pigworker 8/13/2011
@C.A.McCann 如果你以一种相当于同构的单子的方式固定形状,那么所有这些活泼的应用(无论是截断的还是填充的)都限制在单子上。一旦你固定了一个容器的形状,它就会有效地从位置对函数进行编码,就像备忘录一样。彼得·汉考克(Peter Hancock)称这种函子为“Naperian”,因为它们遵循对数定律。Reader
4赞 pigworker 8/13/2011
@stephen tetley:其他例子包括常数幺半数应用(它是单子的组合,但不是单子)和单位延迟应用(最好不要允许连接)。

答:

44赞 Rotsor 8/12/2011 #1

如果有 applicatives 和 ,则该类型也是 appicive(您可以以通用方式编写这样的实例)。A1A2data A3 a = A3 (A1 (A2 a))

另一方面,如果你有单子,那么类型不一定是单子(组合没有合理的泛型实现)。M1M2data M3 a = M3 (M1 (M2 a))>>=join

一个例子可以是类型(这里我们用 组成一个类型构造函数,这两个构造函数都是单子)。你可以很容易地写[Int -> a][](->) Int

app :: [Int -> (a -> b)] -> [Int -> a] -> [Int -> b]
app f x = (<*>) <$> f <*> x

这推广到任何应用:

app :: (Applicative f, Applicative f1) => f (f1 (a -> b)) -> f (f1 a) -> f (f1 b)

但是没有合理的定义

join :: [Int -> [Int -> a]] -> [Int -> a]

如果你不相信这一点,请考虑以下表达:

join [\x -> replicate x (const ())]

在提供整数之前,必须确定返回列表的长度,但其正确的长度取决于提供的整数。因此,此类型不能存在正确的函数。join

评论

1赞 andrew cooke 8/12/2011
...所以当一个函数可以做的时候,避免单子呢?
2赞 Rotsor 8/12/2011
@andrew,如果你的意思是函子,那么是的,函子更简单,应该在足够的时候使用。请注意,并非总是如此。例如,如果没有 a 将很难编程。:)IOMonad
18赞 Landei 8/12/2011 #2

不幸的是,我们的真正目标,单子的组成,是更多的 难。..事实上,我们 其实可以证明,从某种意义上说,是没有办法的 仅使用 两个单子的操作(参见附录,了解 证明)。因此,我们可能希望形成一个 组合是如果有一些额外的结构连接 两个组件。

组成单子,http://web.cecs.pdx.edu/~mpj/pubs/RR-1004.pdf

评论

4赞 Alexandre C. 8/12/2011
氤;对于不耐烦的读者来说,DR:如果(f?)你能提供自然的转换,你就可以组成单子swap : N M a -> M N a
0赞 C. A. McCann 8/13/2011
@Alexandre C.:我怀疑只是“如果”。并非所有单子变压器都由直接函子组成来描述。例如,既不是也不是,并且大致是 。ContT r m am (Cont r a)Cont r (m a)StateT s m aReader s (m (Writer s a))
0赞 Alexandre C. 8/13/2011
@C. A. McCann:我似乎无法从(M monad、N monad、MN monad、NM monad)到 (there exists swap : MN -> NM natural)。因此,让我们暂时坚持使用“如果”(也许答案在报纸上,我必须承认我很快就查过了)
1赞 C. A. McCann 8/13/2011
@Alexandre C.:仅仅指定构图是单子可能还不够——你还需要某种方式将两个部分与整体联系起来。的存在意味着组合物让两者以某种方式“合作”。另外,请注意,这是某些单子的“交换”特例。实际上也是如此。swapsequenceflip
7赞 pigworker 8/13/2011
要写它,在我看来,您可以使用(适当地)在前面插入一个,在后面插入一个,从 开始,然后使用复合的 得到你的.swap :: N (M x) -> M (N x)returnsfmapMNN (M x) -> M (N (M (N x)))joinM (N x)
125赞 pigworker 8/12/2011 #3

如果我们比较类型

(<*>) :: Applicative a => a (s -> t) -> a s -> a t
(>>=) :: Monad m =>       m s -> (s -> m t) -> m t

我们得到了两个概念的区别。在类型中,表明 中的值可以确定 中的计算行为。单子允许值层和计算层之间的干扰。运算符不允许这种干扰:函数和参数计算不依赖于值。这真的很咬人。比较(s -> m t)(>>=)sm t(<*>)

miffy :: Monad m => m Bool -> m x -> m x -> m x
miffy mb mt mf = do
  b <- mb
  if b then mt else mf

它使用某种效果的结果在两个计算之间做出决定(例如发射导弹和签署停战协议),而

iffy :: Applicative a => a Bool -> a x -> a x -> a x
iffy ab at af = pure cond <*> ab <*> at <*> af where
  cond b t f = if b then t else f

它使用 的值在两个计算的值之间进行选择,并且同时执行了这两个计算,可能会产生悲剧性的影响。abataf

monadic 版本基本上依赖于从值中选择计算的额外能力,这可能很重要。然而,支持这种力量使单子难以组成。如果我们尝试构建“双重绑定”(>>=)

(>>>>==) :: (Monad m, Monad n) => m (n s) -> (s -> m (n t)) -> m (n t)
mns >>>>== f = mns >>-{-m-} \ ns -> let nmnt = ns >>= (return . f) in ???

我们走到了这一步,但现在我们的层都乱七八糟。我们有一个 ,所以我们需要去掉外面的 .正如 Alexandre C 所说,如果我们有合适的n (m (n t))n

swap :: n (m t) -> m (n t)

将内在的和它置换到另一个.njoinn

较弱的“双重应用”更容易定义

(<<**>>) :: (Applicative a, Applicative b) => a (b (s -> t)) -> a (b s) -> a (b t)
abf <<**>> abs = pure (<*>) <*> abf <*> abs

因为各层之间没有干涉。

相应地,最好认识到什么时候你真的需要 s 的额外功能,什么时候可以摆脱支持的刚性计算结构。MonadApplicative

顺便说一句,请注意,尽管编写单子很困难,但它可能比您需要的要多。该类型表示使用 -effects 进行计算,然后使用 -effects 计算到 -value,其中 -effects 在 -effects 开始之前完成(因此需要 )。如果你只是想将-effects与-effects交错,那么构图可能要求太多了!m (n v)mnvmnswapmn

评论

3赞 shj 2/29/2012
对于这个不可靠的例子,你说它“使用 ab 的值在两个计算的值之间进行选择 at 和 af,同时执行了这两个计算,可能会产生悲剧性的影响。Haskell的懒惰本性难道不能保护你免受这种情况的影响吗?如果我有 list = (\b t f -> if b then t else f) : [] 然后执行语句: list <*> pure True <*> pure “hello” <*> pure (error “bad”)....我收到“hello”,但错误从未发生。该代码并不像单子那样安全或可控,但该帖子似乎表明应用会导致严格的评估。总的来说,很棒的帖子!谢谢!
9赞 pigworker 3/1/2012
你仍然可以得到两者的效果,但纯粹的(错误“坏”)没有任何效果。另一方面,如果你尝试 iffy (pure True) (pure “hello”) (error “bad”),你会得到一个 miffy 避免的错误。此外,如果你尝试类似 iffy (pure True) (pure 0) [1,2] 的东西,你会得到 [0,0] 而不是 [0]。应用对它们有一种严格的要求,因为它们构建了固定的计算序列,但正如你所观察到的,这些计算产生的仍然被懒惰地组合在一起。
0赞 ron 6/19/2012
对于任何单子,你总是可以写一个单子转换器,并使用吗?所以你总是可以组成单子,只是更复杂,使用变压器?mnmtn (m t)mt n t
4赞 pigworker 6/19/2012
这样的变压器经常存在,但据我所知,没有规范的方法来生成它们。关于如何解决来自不同单子的交错效应,通常有一个真正的选择,典型的例子是异常和状态。异常是否应该回滚状态更改?这两种选择都有其位置。话虽如此,有一个“自由单子”的东西表达了“任意交织”。,然后 ,并考虑 。这推迟了如何运行交织的选择。data Free f x = Ret x | Do (f (Free f x))data (:+:) f g x = Inl (f x) | Tnr (g x)Free (m :+: n)
1赞 ivan vadovič 8/7/2018
@Arek傅,你忽略了'纯洁'。它是纯粹的(错误“坏”),没有任何影响,或者任何纯粹的(任何东西)。
88赞 Conal 8/16/2011 #4

应用组成,单子不组成。

单子确实会组合,但结果可能不是单子。 相反,两种应用物的组成必然是应用剂。 我怀疑最初陈述的意图是“应用性可以组成,而单子性则不会。改写为“在作文下是封闭的,不是。ApplicativeMonad

评论

26赞 Apocalisp 8/16/2011
此外,任何两个应用物以完全机械的方式组成,而由两个单子组成的单子是特定于该组合物的。
14赞 Edward Kmett 8/16/2011
此外,单子以其他方式构成,两个单子的乘积是一个单子,它只是需要某种分配律的副产品。
1赞 Paul Draper 7/18/2015
@Apocalisp,包括评论,这是最好和最简洁的答案。
7赞 user278559 8/16/2011 #5

分配法解决方案 l : MN -> NM 就足够了

以保证 NM 的单一性。要看到这一点,你需要一个单位和一个多重。我将专注于多重(单位是 unit_N unitM)

NMNM - l -> NNMM - mult_N mult_M -> NM

并不能保证MN是一个单子。

然而,当你有分配法解决方案时,关键的观察就会发挥作用

l1 : ML -> LM
l2 : NL -> LN
l3 : NM -> MN

因此,LM、LN 和 MN 是单子。问题在于 LMN 是否是一个单子(通过

(明尼苏达州)L -> L(MN) 或通过 N(流明) -> (流明)N

我们有足够的结构来制作这些地图。然而,正如 Eugenia Cheng 所观察到的,我们需要一个六边形条件(相当于 Yang-Baxter 方程的表示)来保证任一构造的一元性。事实上,在六边形条件下,两个不同的单子重合。

评论

10赞 Dan Burton 8/16/2011
这可能是一个很好的答案,但它在我的脑海中嗖作响。
1赞 codeshot 12/23/2017
这是因为,使用术语 Applicative 和 haskell 标签,这是一个关于 haskell 的问题,但答案不同。
0赞 tillmo 1/15/2021 #6

这是通过分配律工作进行一些代码制作单子组合。请注意,从任何单子到单子、 和 都存在分配律。另一方面,你不会在 和 中找到这样的(一般)分配律。对于这些,您将需要单子变压器。MaybeEitherWriter[]ReaderState

 {-# LANGUAGE FlexibleInstances #-}
 
 module ComposeMonads where
 import Control.Monad
 import Control.Monad.Writer.Lazy
 
 newtype Compose m1 m2 a = Compose { run :: m1 (m2 a) }
 
 instance (Functor f1, Functor f2) => Functor (Compose f1 f2) where
   fmap f = Compose . fmap (fmap f) . run
 
 class (Monad m1, Monad m2) => DistributiveLaw m1 m2 where
   dist :: m2 (m1 a) -> m1 (m2 a)
 
 instance (Monad m1,Monad m2, DistributiveLaw m1 m2)
           => Applicative (Compose m1 m2) where
     pure = return
     (<*>) = ap
 
 instance (Monad m1, Monad m2, DistributiveLaw m1 m2)
           => Monad (Compose m1 m2) where
   return = Compose . return . return
   Compose m1m2a >>= g =
     Compose $ do m2a <- m1m2a -- in monad m1
                  m2m2b <- dist $ do a <- m2a  -- in monad m2
                                     let Compose m1m2b = g a
                                     return m1m2b
                                  -- do ... ::  m2 (m1 (m2 b))
                           -- dist ... :: m1 (m2 (m2 b))          
                  return $ join m2m2b -- in monad m2
 
 instance Monad m => DistributiveLaw m Maybe where
   dist Nothing = return Nothing
   dist (Just m) = fmap Just m
 
 instance Monad m => DistributiveLaw m (Either s) where
   dist (Left s) = return $ Left s
   dist (Right m) = fmap Right m
 
 instance Monad m => DistributiveLaw m [] where
   dist = sequence
 
 instance (Monad m, Monoid w) => DistributiveLaw m (Writer w) where
   dist m = let (m1,w) = runWriter m
            in do a <- m1
                  return $ writer (a,w)
 
 liftOuter :: (Monad m1, Monad m2, DistributiveLaw m1 m2) =>
                    m1 a -> Compose m1 m2 a
 liftOuter = Compose . fmap return
 
 liftInner :: (Monad m1, Monad m2, DistributiveLaw m1 m2) =>
                    m2 a -> Compose m1 m2 a
 liftInner = Compose . return
 
 
   
 
2赞 winitzki 5/19/2021 #7

可以组成任意两个应用函子并产生另一个应用函子。但这不适用于单子。两个单子的组合并不总是一个单子。例如,和 的组合(按任何顺序)不是单子。StateList

此外,一般情况下,无论是通过组合还是通过任何其他方法,都不能组合两个单子。没有已知的算法或过程将任何两个单子组合成一个更大的、合法的单子,以便您可以通过单子形态注入 和 并满足合理的非简并定律(例如,保证这不仅仅是一个丢弃 和 的所有效应的单元类型)。MNTM ~> TN ~> TTMN

可以定义一个适合特定的 和 ,例如 和 等等。但目前尚不清楚如何定义在单子和 .函子组合和更复杂的结构都不能充分工作。TMNM = MaybeN = State sTMN

组合单子的一种方法是,首先定义共同产品。这将是一个函子,但通常不是一个单子。然后在函子上构造一个自由的单子 ()。结果是一个能够表示 和 组合效果的单子。然而,它是一个更大的单子,也可以代表其他效果;它比 和 的效果组合要大得多。此外,自由单子需要被“运行”或“解释”才能提取任何结果(并且单子定律只有在“运行”后才能得到保证)。将存在运行时损失和内存大小损失,因为自由单子可能会在“运行”之前在内存中构建非常大的结构。如果这些缺点不明显,那么免费单子就是要走的路。MNC a = Either (M a) (N a)CFree CCMNMN

组合单子的另一种方法是将一个单子的变压器应用于另一个单子。但是没有算法方法可以定义单子(例如,Haskell 中的类型和代码)并生成相应转换器的类型和代码。

至少有 4 种不同类别的单子,它们的变压器以完全不同但规则的方式构造(内部组合、外部组合、基于连接的单子、乘积单子)。其他一些单子不属于这些“常规”类中的任何一个,并且以某种方式将变压器定义为“临时”。

分配律只存在于组合单子中。认为任何两个单子,可以定义一些函数,就可以组成,这是误导性的。除了定义具有这种类型签名的函数之外,还需要证明某些定律成立。在许多情况下,这些法律并不成立。MNM (N a) -> N (M a)

甚至有些单子有两个不等效的变压器;一个以“常规”方式定义,一个以“临时”方式定义。一个简单的例子是身份单子;它有规则的变压器(“组合”)和不规则的“ad hoc”变压器:(上的共密度单子)。Id a = aIdT m = mIdT2 m a = forall r. (a -> m r) -> m rm

一个更复杂的例子是“选择器单子”:。这里是一个固定类型,是 monad 的主要类型参数。这个单子有两个变压器:(内部组成)和(临时)。Sel q a = (a -> q) -> aqaSel qSelT1 m a = (m a -> q) -> m aSelT2 m a = (a -> m q) -> m a

完整的细节在“函数式编程的科学”一书的第14章中得到了阐述。https://github.com/winitzki/sofphttps://leanpub.com/sofp/