Конкретный пример, показывающий, что монады не закрыты по составу (с доказательством)?

Хорошо известно, что аппликативные функторы замкнуты по композиции, но монады - нет. Однако мне не удалось найти конкретный контрпример, показывающий, что монады не всегда сочиняют.

Этот ответ дает [String -> a] как пример немоноды. Немного поиграв с ним, я считаю это интуитивно, но этот ответ просто говорит: "Присоединение не может быть реализовано", без каких-либо оправданий. Мне хотелось бы что-то более формальное. Конечно, существует множество функций с типом [String -> [String -> a]] -> [String -> a]; нужно показать, что любая такая функция обязательно не удовлетворяет законам монады.

Любой пример (с сопроводительным доказательством) будет делать; Я не обязательно ищут доказательство вышеприведенного примера в частности.

Ответы

Ответ 1

Рассмотрим эту монаду, изоморфную монаде (Bool ->):

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

и составьте его с монадой Maybe:

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 Nothing или B (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 Nothing или B (Just (P y1 y2)), где y1, y2 должны быть выбраны из 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), это означает, что мы должны выбрать y1 из x1 или x3, и y2 от x2 или 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))

Аналогично, это говорит о том, что мы должны выбрать y1 из x1 или x2 и y2 из x3 или 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'')

Ответ 2

Для небольшого конкретного контрпримера рассмотрим терминальную монаду.

data Thud x = Thud

return и >>= просто идут Thud, а законы выполняются тривиально.

Теперь пусть также есть монада-писатель для Bool (с, скажем, структурой xor-моноидов).

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

Er, um, нам понадобится композиция

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

Теперь попробуйте определить...

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

Параметричность говорит нам, что ??? не может быть каким-либо полезным образом на x, поэтому он должен быть константой. В результате join . return обязательно является постоянной функцией, поэтому закон

join . return = id

должен завершиться ошибкой для любых определений join и return.

Ответ 3

Построение исключенного среднего

(->) 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 были монадой для каждых r и e, тогда мы могли бы реализовать закон исключенного среднего. Это невозможно в интуиционистской логике, которая лежит в основе систем типов функциональных языков (имеющий закон исключенного среднего, эквивалентен вызову /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, только с добавленными конструкторами newtype (de). Обратите внимание, что нам все равно, какие свойства у него есть, что он является определяемым и тотальным.)

Теперь пусть set

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

и специализируйте swap для r = b, e = b, a = False:

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

Вывод: Даже если (->) r и Either r являются монадами, их состав Comp r r не может быть.

Примечание. Это также отражено в ReaderT и EitherT. Оба ReaderT r (Either e) и EitherT e (Reader r) изоморфны r -> Either e a! Нет способа определить монаду для двойного 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)

Что произойдет, когда эта программа будет запущена? Возможны две возможности:

  • "Первая" или "Вторая" печатается после того, как мы читаем input с консоли, что означает, что последовательность действий была отменена и что действия из foo вырвались в чистую f.
  • Или swap (следовательно, join) отбрасывает действие IO, и ни "First", ни "Second" никогда не печатаются. Но это означает, что 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, либо они должны выбросить его и заменить чем-то другим, нарушив закон.

Ответ 4

Ваша ссылка ссылается на этот тип данных, поэтому попробуйте выбрать определенную реализацию: data A3 a = A3 (A1 (A2 a))

Я произвольно возьму A1 = IO, A2 = []. Мы также сделаем это newtype и придаем ему особенно острое имя для удовольствия:

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 - коммутативная монада. Это препятствует тому, чтобы большая группа монадов составляла [], что нарушает универсальное правило "составления двух произвольных монадов, дает монаду".

Смотрите также: fooobar.com/info/79553/...