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


82

Загальновідомо, що аплікативні функтори закриті під композицією, але монади - ні. Однак у мене були проблеми з конкретним контраприкладом, який показує, що монади складаються не завжди.

Ця відповідь наводить [String -> a]як приклад немонади. Трохи погравши з цим, я вірю в це інтуїтивно, але ця відповідь просто говорить: "приєднання не може бути реалізоване", насправді не давши жодного обґрунтування. Я хотів би чогось більш офіційного. Звичайно, існує безліч функцій з типом [String -> [String -> a]] -> [String -> a]; треба показати, що будь-яка така функція неодмінно не задовольняє законам монади.

Будь-який приклад (із супровідним доказом) підійде; Я не обов'язково шукаю доказ, зокрема, наведеного вище прикладу.


Найближчим, що я можу знайти, є додаток web.cecs.pdx.edu/~mpj/pubs/RR-1004.pdf , який показує, що за багатьох спрощуючих припущень неможливо писати joinпро склад двох монад у загальний . Але це не приводить до якихось конкретних прикладів.
Brent Yorgey

Ви можете отримати кращі відповіді на це запитання на cs.stackexchange.com, новому сайті обміну стеками комп’ютерних наук.
Patrick87,

3
Можливо, я не розумію, але я думаю, що питання можна було б визначити точніше. Коли ви говорите "складання" двох монад, ви маєте на увазі просто складання конструкторів типу? І коли результат "не є монадою", чи означає це, що екземпляр монади такого типу конструкор не може бути записаний? І, якщо екземпляр монади для складеного конструктора типу може бути записаний, чи повинен він мати якесь відношення до екземплярів двох факторних монад, або він може бути абсолютно не пов’язаний?
Оуен,

1
Так, я маю на увазі складання конструкторів типу; "не монада" означає дійсний (законний) екземпляр монади не може бути записаний; і мені байдуже, чи має екземпляр композиції якесь відношення до екземплярів факторів.
Brent Yorgey

Відповіді:


42

Розглянемо цю монаду, яка є ізоморфною (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))в разі (Е), це означає , що ми повинні вибрати 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'')

Дякую, я переконана! Хоча це змушує мене задуматися, чи є способи спростити ваш доказ.
Brent Yorgey

1
@BrentYorgey: Я підозрюю, що так і повинно бути, оскільки проблеми зі справами (C) та (D), схоже, дуже нагадують проблеми, які виникають у вас під час спроби визначити swap :: Pair (Maybe a) -> Maybe (Pair a).
hammar

11
Отже, коротко: монадам дозволено викидати інформацію, і це нормально, якщо вони просто вкладені самі в себе. Але коли у вас є монада, що зберігає інформацію, і монада, що знижує інформацію, поєднуючи ці дві краплі інформації, хоча той, хто зберігає інформацію, потребує цієї інформації, щоб задовольнити свої власні закони про монаду. Тож не можна поєднувати довільні монади. (Ось чому вам потрібні прохідні монади, які гарантують, що вони не втратять відповідну інформацію; їх можна довільно складати.) Дякуємо за інтуїцію!
Xanthir

@Xanthir Compose працює лише в одному порядку: (Maybe a, Maybe a)є монадою (оскільки є продуктом двох монад), але Maybe (a, a)не є монадою. Я також перевірив, що Maybe (a,a)не є монадою шляхом явних розрахунків.
winitzki

Розум показує, чому Maybe (a, a)це не монада? І "Можливо", і "Кортеж" є прохідними, і їх слід складати в будь-якому порядку; Є й інші запитання щодо SO, які також говорять про цей конкретний приклад.
Ксантір

38

Для невеликого конкретного контраприкладу розглянемо кінцеву монаду.

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

Е-е, нам буде потрібно склад

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

повинні зазнати невдачі за будь-якими визначеннями, joinі returnми обрали.


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монада була для кожного rі eтоді, ми могли б реалізувати закон витісненого середнього . Це неможливо в інтуїціоністичній логіці, яка лежить в основі системних типів функціональних мов (якщо закон виключеного середнього еквівалентний наявності оператора 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) newtype. Зверніть увагу, що нам байдуже, якими властивостями вона володіє, важливо лише те, щоб вона була визначною та сумарною .)

Тепер давайте встановимо

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

і спеціалізується на своп 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)

Що станеться після запуску цієї програми? Є дві можливості:

  1. "Перший" або "Другий" друкується після того, як ми прочитали inputз консолі, що означає, що послідовність дій була змінена і що дії з fooперейшли в чисту f.
  2. Або swap(отже join) відкидає IOдію, і ні "Перший", ні "Другий" ніколи не друкуються. Але це означає, що це joinпорушує закон

    join . return = id
    

    бо якщо joinвідкине IOдію, тоді

    foo ≠ (join . return) foo
    

Інші подібні IOпоєднання + монада ведуть до побудови

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. Не могли б ви розширити його? У статті, на яку посилається Брент, видно, що для переходу від joinдо swapнас потрібні наступні припущення: joinM . fmapM join = join . joinMа join . fmap (fmapM joinN ) = fmapM joinN . join де joinM = join :: M тощо
Рафаель Каетано

1
@RafaelCaetano Дякую за помилку, я її виправив (а також перейменував функцію swapна відповідність паперу). Я не перевіряв документ досі, і ви маєте рацію, схоже, нам потрібні J (1) і J (2) для визначення swap<-> join. Це, мабуть, слабке місце мого доказу, і я буду думати над цим більше (можливо, можна було б щось отримати з того, що це Applicative).
Петр

@RafaelCaetano Але я вважаю, що доказ все ще є дійсним: якби ми мали join, ми могли б визначити, swap :: (Int -> Maybe a) -> Maybe (Int -> a)використовуючи наведене вище визначення (незалежно від того, яким законам це swapвідповідає). Як би swapповодився такий ? Не маючи ні Int, він не має чого передати своєму аргументу, тому йому довелося б повернутися Nothingдля всіх входів. Я вважаю , що ми можемо отримати протиріччя для joinзаконів Монада "S без необхідності визначити joinз swapспини. Я перевірю це і повідомлю вас.
Петр

@Petr: По суті, я погоджуюсь з Рафаелем, що це не зовсім той доказ, який я шукаю, але мені також цікаво дізнатись, чи можна це виправити у відповідності до згаданих вами ліній.
Brent Yorgey

1
@ PetrPudlák Ого, дуже приємно! Так, я повністю купую його зараз. Це справді цікаві ідеї. Я б не здогадався, що просто спроможність побудувати своп може призвести до суперечності, взагалі не посилаючись на жоден із законів монади! Якби я міг прийняти кілька відповідей, я б прийняв і цю.
Brent Yorgey

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є комутативною монадою. Це перешкоджає композиції великої категорії монад [], що порушує універсальне правило "складання двох довільних монад дає монаду".

Дивіться також: https://stackoverflow.com/a/12617918/1769569


11
Я думаю, це лише показує, що одне конкретне визначення ListTне дає змоги створити монаду у всіх випадках, а не показує, що жодне можливе визначення не може працювати.
CA McCann

Мені не треба. Заперечення "для всього цього, тобто" є "існує контра-приклад". Задавалося питання: "для всіх монад їх склад утворює монаду". Я показав комбінацію типів, які є монадами самі по собі, але не можуть складати.
hpc

11
@hpc, але склад двох монад більше, ніж склад їх типів. Вам також потрібні операції, і моє тлумачення запитання Брента полягає в тому, що може не існувати методичний спосіб отримати реалізацію операцій - він шукає щось ще сильніше, що деякі композиції не мають операцій, що відповідають законам, механічно вивідний чи ні. Чи має це сенс?
luqui

Так, Лукі це правильно. Вибачте, якщо моє початкове запитання було незрозумілим.
Brent Yorgey

У цій відповіді насправді бракує Monadекземпляру ListTта демонстрації, що інших немає. Твердження: "для всього цього існує те, що існує", і тому заперечення "існує таке, що для всього цього"
Бен Міллвуд,

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.