Програми складаються, монади ні


110

Програми складаються, монади ні.

Що означає вищезазначене твердження? А коли одне переважне перед іншим?


5
Звідки ви взяли цю заяву? Можливо, буде корисно побачити якийсь контекст.
fuz

@FUZxxl: Я чув це неодноразово від багатьох різних людей, останнім часом від debasishg на Twitter.
зниклий фактор

3
@stephen Тетлі: Зверніть увагу , що багато з таких Applicativeроків, на самому ділі ціла сім'я з Monadх, а саме : по одному для кожної «форми» структури можливої. ZipListне є Monad, але ZipLists фіксованої довжини є. Reader- це зручний спеціальний (чи це загальний?) випадок, коли розмір "структури" фіксується як простота типу середовища.
CA McCann

3
@CAMcCann Усі ці додатки на блискавці (будь то усікання або накладка) обмежуються монадами, якщо ви фіксуєте форму таким чином, що дорівнює Readerмонаді аж до ізоморфізму. Після того, як ви виправите форму контейнера, він ефективно кодує функцію з позицій, як трие пам’яті. Пітер Хенкок називає таких функторів "напіреєць", оскільки вони підкоряються законам логарифмів.
pigworker

4
@stephen tetley: Інші приклади включають додаток з постійними моноїдами (який є монадою, але не монадою), і додаток для затримки одиниці (якому краще не допустити приєднання).
pigworker

Відповіді:


115

Якщо порівнювати типи

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

ми отримуємо підказку до того, що розділяє ці два поняття. Це (s -> m t)за типом (>>=)показує, що значення в sможе визначати поведінку обчислення в m 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

яке використовує значення abдля вибору між значеннями двох обчислень atтаaf , провівши обидва, можливо, для трагічного ефекту.

Монадічна версія по суті покладається на додаткову силу (>>=)вибору обчислення зі значення, і це може бути важливо. Однак підтримка цієї влади робить монадів складно скласти. Якщо ми спробуємо побудувати "подвійне зв'язування"

(>>>>==) :: (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 ???

ми дістаємось так далеко, але тепер наші шари все підскочили. У нас є n (m (n t)), тому нам потрібно позбутися зовнішньої n. Як каже Олександр С, ми можемо це зробити, якщо маємо підходящий

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

переставляти nнутрощі та joinіншеn .

Слабше "подвійне застосування" визначити набагато простіше

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

тому що між шарами немає перешкод.

Відповідно, добре визнати, коли вам справді потрібна додаткова потужність Monads, і коли ви можете піти з жорсткої структури обчислень, яка Applicativeпідтримує.

Зауважте, до речі, що хоча складати монади складно, це може бути більше, ніж потрібно. Тип m (n v)вказує обчислення з m-ефектами, потім обчислення з n-ефектами до -значення v, де m-ефекти закінчуються до початку n-ефектів (звідси необхідність swap). Якщо ви просто хочете mпереплутати -ефекти з n-ефектами, то композиція, можливо, занадто багато, щоб запитати!


3
У прикладі iffy ви заявляєте, що воно "використовує значення ab для вибору між значеннями двох обчислень at і af, провівши обидва, можливо, для трагічного ефекту". Чи не захищає вас від цього ледачий характер Хаскелла? Якщо у мене є list = (\ btf -> if b, то t else f): [], а потім виконати оператор: list <*> pure True <*> pure "привіт" <*> pure (помилка "погано"). ... Я отримую "привіт", і помилка ніколи не виникає. Цей код не є настільки безпечним або контрольованим, як монада, але публікація здається, що це говорить про те, що програми вимагають суворої оцінки. Загалом чудовий пост, хоча! Дякую!
shj

7
Ви все одно отримуєте наслідки обох, але чистих (помилка "погано") не має. Якщо, з іншого боку, ви спробуєте iffy (чистий True) (чистий "привіт") (помилка "поганий"), ви отримаєте помилку, яку міфі уникає. Більше того, якщо ви спробуєте щось на зразок iffy (pure True) (чистий 0) [1,2], ви отримаєте [0,0] замість [0]. Програми мають певну суворість щодо них, оскільки вони будують фіксовану послідовність обчислень, але значення, отримані в результаті цих обчислень, все ще ліниво поєднуються, як ви зауважуєте.
свинарник

Чи правда, що для будь-яких монад mі nви завжди можете написати трансформатор монади mtта працювати з n (m t)використанням mt n t? Тож ви завжди можете складати монади, це просто складніше, використовуючи трансформери?
ron

4
Такі трансформатори часто існують, але, наскільки я знаю, немає ніякого канонічного способу їх генерації. Часто існує справжній вибір щодо того, як розв'язати міжрядкові ефекти різних монад, класичним прикладом є винятки та стан. Чи повинен виняток змінювати стан відхилення чи ні? Обидва варіанти мають своє місце. Сказавши це, є "вільна монада", що виражає "довільне перемежування". data Free f x = Ret x | Do (f (Free f x)), то data (:+:) f g x = Inl (f x) | Tnr (g x)і розглянемо Free (m :+: n). Це затримує вибір способу запуску переплетень.
свинарник

@pigworker Щодо лінивої / суворої дискусії. Я думаю, що за допомогою додатків ви не можете керувати ефектом в межах обчислення, але рівень ефекту цілком може вирішити не оцінювати більш пізні значення. Для (прикладних) парсерів це означає, що якщо аналізатор не працює рано, наступні парсери не оцінюються / не застосовуються до вводу. Для Maybeце означає , що в початку Nothingбуде придушувати оцінку aбільш пізнім / наступний Just a. Це правильно?
ziggystar

75

Програми складаються, монади ні.

Монада зробити складати, але результат не може бути монадой. На противагу цьому, склад двох додатків обов'язково є додатком. Я підозрюю, що наміром оригінального твердження було те, що "придатність складається, а монадність - ні". Перефразоване, " Applicativeзакривається під композицію, і Monadні".


24
Крім того, будь-які два додатки складаються повністю механічним способом, тоді як монада, утворена складом двох монад, є специфічною для цього складу.
Апокаліпс

12
Більше того, монади складаються по-іншому, твір двох монад - це монада, лише копродукти потребують певного закону розподілу.
Едвард КМЕТТ

З, @Apocalisp, коментар включений, це найкраща і найкоротша відповідь.
Пол Дрейпер

39

Якщо у вас є додатки, A1а A2потім - типdata A3 a = A3 (A1 (A2 a)) також апплікатівен (ви можете написати такий екземпляр в загальному вигляді).

З іншого боку, якщо у вас є монади M1і M2тоді тип data 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

2
@andrew, якщо ви мали на увазі функтора, то так, функтори простіші і їх слід використовувати, коли достатньо. Зауважте, що це не завжди. Наприклад, IOбез цього Monadбуло б дуже важко програмувати. :)
Ротсор

17

На жаль, наша реальна мета, склад монад, є досить важкою. .. Насправді ми можемо фактично довести, що в певному сенсі немає способу побудувати функцію приєднання з типом, описаним вище, використовуючи лише операції двох монад (див. Додаток до контуру доказу). Звідси випливає, що єдиним способом, на який ми можемо сподіватися сформувати композицію, є наявність додаткових конструкцій, що з'єднують ці дві компоненти.

Складаються монади, http://web.cecs.pdx.edu/~mpj/pubs/RR-1004.pdf


4
Tl; dr для нетерплячих читачів: ви можете скласти монади, якщо (f?) Ви зможете забезпечити природне перетворенняswap : N M a -> M N a
Олександр C.

@ Олександр С .: Просто "якщо", підозрюю. Не всі монадні трансформатори описуються композицією прямого функтора. Наприклад, ContT r m aне є ні m (Cont r a)ні Cont r (m a), і StateT s m aє приблизно Reader s (m (Writer s a)).
CA McCann

@CA McCann: Я, здається, не можу перейти від (M monad, N monad, MN monad, NM monad) до (існує своп: MN -> NM natural). Тож давайте дотримуватимемося "якщо" зараз (можливо, відповідь є у газеті, мушу, зізнаюся, я швидко її подивився)
Олександр К.

1
@Alexandre C .: Тільки уточнюючи, що композиції є монадами, все одно може бути недостатньо - вам також потрібен певний спосіб співвідносити дві частини з цілим. З існування swapвипливає, що композиція дозволяє їм якось «співпрацювати». Також зауважте, що sequenceце окремий випадок "своп" для деяких монад. Так і є flipнасправді.
CA McCann

7
Для написання swap :: N (M x) -> M (N x)мені здається, що ви можете використовувати returns(відповідно fmapпед), щоб вставити Mпередню частину і Nзадню частину ззаду N (M x) -> M (N (M (N x))), а потім використовувати joinкомпозит, щоб отримати своє M (N x).
pigworker

7

Рішення закону розподілу l: MN -> 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 монадою (або від

(MN) L -> L (MN) або по N (LM) -> (LM) N

У нас достатньо структури, щоб зробити ці карти. Однак, як зауважує Євгенія Чен , нам потрібна гексагональна умова (що означає подання рівняння Ян-Бакстера), щоб гарантувати одноманітність будь-якої конструкції. Насправді, з шестикутною умовою збігаються дві різні монади.


9
Це, мабуть, чудова відповідь, але він пройшов повз дорогу через мою голову.
Ден Бертон

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