一个具体的例子表明单子在组合下没有关闭(有证据)?


82

众所周知,应用函子根据组成是封闭的,但单子函数不是。但是,我一直在寻找一个具体的反例来说明单子并不总是构成的麻烦。

这个答案给出[String -> a]了一个非单子的例子。在玩了一段时间之后,我凭直觉相信了它,但是这个答案只是说“无法实现连接”而没有给出任何理由。我想要更正式的东西。当然有很多类型的函数[String -> [String -> a]] -> [String -> a]; 必须证明任何此类功能不一定满足单子法则。

任何示例(附带证明)都可以;我不一定要特别寻找上述示例的证明。


我能找到的最接近的是阑尾web.cecs.pdx.edu/~mpj/pubs/RR-1004.pdf,这表明,在很多简化假设,这是不可能写join两个单子的组成中一般的。但这并没有导致任何具体的例子。
布伦特·约热

在新的Computer Science Stack Exchange网站cs.stackexchange.com上,您可能会得到更好的答案。
Patrick87,2012年

3
也许我没有理解,但是我认为这个问题可以更精确地定义。当您说“组成”两个单子时,您是说简单地组成类型构造函数吗?当结果“不是monad”时,这是否意味着无法编写该类型的construcor monad实例?而且,如果可以为组合类型构造函数编写一个monad实例,它是否必须与两个因子monad的实例有任何关系,还是可以完全不相关?
欧文(Owen)2012年

1
是的,我的意思是组成类型构造函数。“非单子”表示无法编写有效(合法)单子实例;而且我不在乎组成实例是否与因素实例有任何关系。
布伦特·约热

Answers:


42

考虑与(Bool ->)monad同构的monad:

data Pair a = P a a

instance Functor Pair where
  fmap f (P x y) = P (f x) (f y)

instance Monad Pair where
  return x = P x x
  P a b >>= f = P x y
    where P x _ = f a
          P _ y = f b

并用Maybemonad组成:

newtype Bad a = B (Maybe (Pair a))

我声称那Bad不可能是单子。


部分证明:

只有一种定义fmap满足的方法fmap id = id

instance Functor Bad where
    fmap f (B x) = B $ fmap (fmap f) x

回忆一下单子法则:

(1) join (return x) = x 
(2) join (fmap return x) = x
(3) join (join x) = join (fmap join x)

对于的定义return x,我们有两个选择:B NothingB (Just (P x x))。显然,为了有希望x从(1)和(2)返回,我们不能丢掉它x,因此我们必须选择第二个选项。

return' :: a -> Bad a
return' x = B (Just (P x x))

离开join。由于只有少数可能的输入,因此我们可以为每个输入一个案例:

join :: Bad (Bad a) -> Bad a
(A) join (B Nothing) = ???
(B) join (B (Just (P (B Nothing)          (B Nothing))))          = ???
(C) join (B (Just (P (B (Just (P x1 x2))) (B Nothing))))          = ???
(D) join (B (Just (P (B Nothing)          (B (Just (P x1 x2)))))) = ???
(E) join (B (Just (P (B (Just (P x1 x2))) (B (Just (P x3 x4)))))) = ???

由于输出具有类型Bad a,唯一的选择是B NothingB (Just (P y1 y2))其中y1y2必须从选择x1 ... x4

在情况(A)和(B)中,我们没有类型的值a,因此B Nothing在两种情况下我们都必须返回。

情况(E)由(1)和(2)单子法确定:

-- apply (1) to (B (Just (P y1 y2)))
join (return' (B (Just (P y1 y2))))
= -- using our definition of return'
join (B (Just (P (B (Just (P y1 y2))) (B (Just (P y1 y2))))))
= -- from (1) this should equal
B (Just (P y1 y2))

为了返回B (Just (P y1 y2))情况(E),这意味着我们必须y1x1或中进行选择x3,并y2x2或中进行选择x4

-- apply (2) to (B (Just (P y1 y2)))
join (fmap return' (B (Just (P y1 y2))))
= -- def of fmap
join (B (Just (P (return y1) (return y2))))
= -- def of return
join (B (Just (P (B (Just (P y1 y1))) (B (Just (P y2 y2))))))
= -- from (2) this should equal
B (Just (P y1 y2))

同样,这表示我们必须y1x1或中进行选择x2,并y2x3或中进行选择x4。结合两者,我们确定(E)的右侧必须为B (Just (P x1 x4))

到目前为止,一切都很好,但是当您尝试在(C)和(D)的右侧填写时,问题就来了。

每种都有5个可能的右侧,并且所有组合都不起作用。我对此还没有一个很好的论据,但是我有一个可以详尽测试所有组合的程序:

{-# LANGUAGE ImpredicativeTypes, ScopedTypeVariables #-}

import Control.Monad (guard)

data Pair a = P a a
  deriving (Eq, Show)

instance Functor Pair where
  fmap f (P x y) = P (f x) (f y)

instance Monad Pair where
  return x = P x x
  P a b >>= f = P x y
    where P x _ = f a
          P _ y = f b

newtype Bad a = B (Maybe (Pair a))
  deriving (Eq, Show)

instance Functor Bad where
  fmap f (B x) = B $ fmap (fmap f) x

-- The only definition that could possibly work.
unit :: a -> Bad a
unit x = B (Just (P x x))

-- Number of possible definitions of join for this type. If this equals zero, no monad for you!
joins :: Integer
joins = sum $ do
  -- Try all possible ways of handling cases 3 and 4 in the definition of join below.
  let ways = [ \_ _ -> B Nothing
             , \a b -> B (Just (P a a))
             , \a b -> B (Just (P a b))
             , \a b -> B (Just (P b a))
             , \a b -> B (Just (P b b)) ] :: [forall a. a -> a -> Bad a]
  c3 :: forall a. a -> a -> Bad a <- ways
  c4 :: forall a. a -> a -> Bad a <- ways

  let join :: forall a. Bad (Bad a) -> Bad a
      join (B Nothing) = B Nothing -- no choice
      join (B (Just (P (B Nothing) (B Nothing)))) = B Nothing -- again, no choice
      join (B (Just (P (B (Just (P x1 x2))) (B Nothing)))) = c3 x1 x2
      join (B (Just (P (B Nothing) (B (Just (P x3 x4)))))) = c4 x3 x4
      join (B (Just (P (B (Just (P x1 x2))) (B (Just (P x3 x4)))))) = B (Just (P x1 x4)) -- derived from monad laws

  -- We've already learnt all we can from these two, but I decided to leave them in anyway.
  guard $ all (\x -> join (unit x) == x) bad1
  guard $ all (\x -> join (fmap unit x) == x) bad1

  -- This is the one that matters
  guard $ all (\x -> join (join x) == join (fmap join x)) bad3

  return 1 

main = putStrLn $ show joins ++ " combinations work."

-- Functions for making all the different forms of Bad values containing distinct Ints.

bad1 :: [Bad Int]
bad1 = map fst (bad1' 1)

bad3 :: [Bad (Bad (Bad Int))]
bad3 = map fst (bad3' 1)

bad1' :: Int -> [(Bad Int, Int)]
bad1' n = [(B Nothing, n), (B (Just (P n (n+1))), n+2)]

bad2' :: Int -> [(Bad (Bad Int), Int)]
bad2' n = (B Nothing, n) : do
  (x, n')  <- bad1' n
  (y, n'') <- bad1' n'
  return (B (Just (P x y)), n'')

bad3' :: Int -> [(Bad (Bad (Bad Int)), Int)]
bad3' n = (B Nothing, n) : do
  (x, n')  <- bad2' n
  (y, n'') <- bad2' n'
  return (B (Just (P x y)), n'')

谢谢,我坚信!尽管这确实使我怀疑是否有简化您的证明的方法。
布伦特·约基(Brent Yorgey)2012年

1
@BrentYorgey:我怀疑应该有,因为案例(C)和(D)的问题似乎非常像您在尝试定义时遇到的问题swap :: Pair (Maybe a) -> Maybe (Pair a)
hammar 2012年

11
因此,简而言之:单子允许丢弃信息,如果它们只是嵌套在自己体内也可以。但是,当您有一个保留信息的monad和一个丢弃信息的monad时,将两个丢弃信息组合在一起,即使保留信息的人需要保留该信息以满足其自己的monad法则。因此,您不能合并任意单子。(这就是为什么您需要遍历单子的原因,这保证了它们不会丢失相关信息;它们是任意组合的。)感谢您的直觉!
Xanthir

@Xanthir组合仅按一个顺序工作:(Maybe a, Maybe a)是一个monad(因为它是两个monad的乘积),但Maybe (a, a)不是monad。我还Maybe (a,a)通过显式计算验证了这不是单子。
winitzki '18

介意显示为什么Maybe (a, a)不是单子?Maybe和Tuple都是可遍历的,并且可以以任何顺序组合。还有其他SO问题也讨论此特定示例。
Xanthir '18年

38

对于一个小的具体反例,请考虑终端monad。

data Thud x = Thud

return>>=只是去Thud,和法律保持平凡。

现在,让我们还有Bool的作家monad(具有xor-monoid结构)。

data Flip x = Flip Bool x

instance Monad Flip where
   return x = Flip False x
   Flip False x  >>= f = f x
   Flip True x   >>= f = Flip (not b) y where Flip b y = f x

嗯,我们需要组成

newtype (:.:) f g x = C (f (g x))

现在尝试定义...

instance Monad (Flip :.: Thud) where  -- that's effectively the constant `Bool` functor
  return x = C (Flip ??? Thud)
  ...

参数告诉我们???不能以任何有用的方式依赖x,因此它必须是一个常数。结果,join . return也必然是一个常数函数,因此定律

join . return = id

对于任何定义都必须失败joinreturn我们选择。


3
在卡罗的Hamalainen博客有额外的,非常清晰,我已经发现的有用上述回答的详细分析:carlo-hamalainen.net/blog/2014/1/2/...
paluh

34

构造排除中间

(->) r是每一个单子r,并Either e为每一个单子e。让我们定义它们的组成((->) r内部,Either e外部):

import Control.Monad
newtype Comp r e a = Comp { uncomp :: Either e (r -> a) }

我声称,如果Comp r e每个人都是一个单子re那么我们就可以实现排除中间律。这在功能语言的类型系统基础上的直觉逻辑中是不可能的(具有排除中间法则等效于使用call / cc运算符)。

假设Comp是单子。那我们有

join :: Comp r e (Comp r e a) -> Comp r e a

所以我们可以定义

swap :: (r -> Either e a) -> Either e (r -> a)
swap = uncomp . join . Comp . return . liftM (Comp . liftM return)

(这只是布伦特提到的swap纸张合成单子的功能,第4.3节,仅添加了新类型的(de)构造函数。请注意,我们不在乎它具有什么属性,唯一重要的是它是可定义的且具有总计)

现在开始

data False -- an empty datatype corresponding to logical false
type Neg a = (a -> False) -- corresponds to logical negation

并专门调换r = be = ba = False

excludedMiddle :: Either b (Neg b)
excludedMiddle = swap Left

结论:即使(->) rEither r是单子,它们的组成Comp r r也不能。

注:这也是体现在如何ReaderTEitherT定义。无论 ReaderT r (Either e)EitherT e (Reader r)同形r -> Either e a!没有办法为double定义monad Either e (r -> a)


逃避IO行动

同一个例子中有许多涉及IO并导致逃逸的例子IO。例如:

newtype Comp r a = Comp { uncomp :: IO (r -> a) }

swap :: (r -> IO a) -> IO (r -> a)
swap = uncomp . join . Comp . return . liftM (Comp . liftM return)

现在让我们

main :: IO ()
main = do
   let foo True  = print "First" >> return 1
       foo False = print "Second" >> return 2
   f <- swap foo
   input <- readLn
   print (f input)

运行该程序会发生什么?有两种可能性:

  1. “第一”或“第二”的印刷,我们读到input从控制台,这意味着操作的顺序被颠倒,并从行动foo逃到纯净f
  2. swap(因此join)放弃IO操作,并且“第一”和“第二”都不会被打印。但这意味着join违反法律

    join . return = id
    

    因为如果join丢掉IO动作,那么

    foo ≠ (join . return) foo
    

其他类似的IO+ monad组合导致构建

swapEither :: IO (Either e a) -> Either e (IO a)
swapWriter :: (Monoid e) => IO (Writer e a) -> Writer e (IO a)
swapState  :: IO (State e a) -> State e (IO a)
...

他们的join实现要么必须e逃避,IO要么必须将其丢弃并替换为其他内容,从而违反法律。


(我想“ ap”是“ fmap,pure和ap是规范定义的地方”的错字(应该<*>改为),试图对其进行编辑,但被告知我的编辑太短了。)---不清楚我认为,具有的定义join意味着的定义swap。您能扩展一下吗?布伦特(Brent)提及的论文似乎暗示, 从头到尾joinswap我们需要以下假设: joinM . fmapM join = join . joinM以及 join . fmap (fmapM joinN ) = fmapM joinN . joinjoinM = join :: M等。–
Rafael Caetano

1
@RafaelCaetano感谢您的拼写错误,我已将其修复(并重命名了功能swap以匹配纸张)。直到现在我都没有检查论文,您是对的,看起来我们需要J(1)和J(2)来定义swap<-> join。这也许是我证明的一个弱点,我会考虑得更多(也许有可能从事实中得到一些东西Applicative)。
彼得

@RafaelCaetano但是我相信证明仍然有效:如果有的话join,我们可以swap :: (Int -> Maybe a) -> Maybe (Int -> a)使用上面的定义进行定义(无论这swap满足什么法律)。这样会如何swap表现?没有no Int,则没有任何东西可以传递给它的参数,因此它必须Nothing为所有输入返回。我相信我们可以为join的单子定律带来矛盾,而无需joinswap后面进行定义。我会检查一下并告知您。
彼得

@Petr:就目前而言,我同意拉斐尔的观点,这并不是我要找的证据,但是我也很好奇,看看它是否可以按照您提到的方式解决。
布伦特·约基(Brent Yorgey)2012年

1
@PetrPudlák哇,太好了!是的,我现在完全买了。这些是一些非常有趣的见解。我不会猜到,仅仅能够构造交换就可能导致矛盾,而根本不参考任何monad法则!如果我可以接受多个答案,我也将接受这一答案。
布伦特·约基(Brent Yorgey)2012年

4

您的链接引用了此数据类型,因此让我们尝试选择一些特定的实现: data A3 a = A3 (A1 (A2 a))

我会随便挑A1 = IO, A2 = []。我们也将其newtype命名为a并给它一个特别尖的名称,以供娱乐:

newtype ListT IO a = ListT (IO [a])

让我们提出该类型的一些任意动作,并以两种不同但相等的方式运行它:

λ> let v n = ListT $ do {putStr (show n); return [0, 1]}
λ> runListT $ ((v >=> v) >=> v) 0
0010101[0,1,0,1,0,1,0,1]
λ> runListT $ (v >=> (v >=> v)) 0
0001101[0,1,0,1,0,1,0,1]

如您所见,这违反了关联定律:∀x y z. (x >=> y) >=> z == x >=> (y >=> z)

事实证明,ListT m如果m可交换单子,则仅是单子。这样可以防止一大类monad与组成[],这打破了“组成两个任意monad产生monad”的通用规则。

另请参阅:https : //stackoverflow.com/a/12617918/1769569


11
我认为这仅表明一个特定的定义ListT在所有情况下都无法产生单子,而不是表明没有任何可能的定义可以起作用。
CA McCann 2012年

不用了 “所有这些,即”的否定是“存在反例”。提出的问题是“对于所有单子,它们的组成都是单子”。我已经展示了一种类型的组合,这些类型本身就是单子,但不能组成。
hpc

11
@hpc,但是两个monad的组成比其类型的组成更多。您还需要操作,而我对布伦特问题的解释是,可能没有一种有条理的方法来推导操作的实现-他正在寻找更强大的东西,某些组合没有满足法律的操作。是否机械衍生。那有意义吗?
luqui 2012年

是的,卢基说对了。抱歉,如果我的原始问题不清楚。
布伦特·尤吉

这个答案真正缺少的是的Monad实例ListT,并且没有其他实例。该声明是“对于所有存在,都存在”,因此否定为“对于所有存在,都存在”
Ben Millwood 2012年

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.