Що таке монада?


1414

Нещодавно коротко подивившись на Haskell, що було б коротким, стислим, практичним поясненням того, що по суті є монадою?

Я знайшов більшість пояснень, які мені трапляються, досить недоступними та не мають практичних деталей.


12
Ерік Ліпперт написав відповідь на це запитання ( stackoverflow.com/questions/2704652/… ), яка через деякі проблеми перебуває на окремій сторінці.
П Швед

70
Ось новий вступ із використанням JavaScript - я вважав його дуже читабельним.
Benjol



2
Монада - це масив функцій з операціями помічників. Дивіться цю відповідь
cibercitizen1

Відповіді:


1059

По-перше: Термін монада трохи вакуумний, якщо ви не математик. Альтернативний термін - це обчислювач, який трохи більше описує те, для чого вони насправді корисні.

Ви запитуєте практичні приклади:

Приклад 1: Ознайомлення зі списком :

[x*2 | x<-[1..10], odd x]

Цей вираз повертає подвоєння всіх непарних чисел у межах від 1 до 10. Дуже корисно!

Виявляється, це дійсно просто синтаксичний цукор для деяких операцій в монаді Лісти. Таке ж розуміння списку можна записати як:

do
   x <- [1..10]
   guard (odd x)
   return (x * 2)

Або навіть:

[1..10] >>= (\x -> guard (odd x) >> return (x*2))

Приклад 2: Вхід / вихід :

do
   putStrLn "What is your name?"
   name <- getLine
   putStrLn ("Welcome, " ++ name ++ "!")

В обох прикладах використовуються монади, будівельники обчислень AKA. Поширена тема полягає в тому, що монада ланцюгує операції якимось конкретним, корисним способом. У розумінні списку операції є ланцюговими таким чином, що якщо операція повертає список, то наступні операції виконуються на кожному елементі списку. Монада вводу-виводу з іншого боку виконує операції послідовно, але передає вздовж "приховану змінну", яка представляє "стан світу", що дозволяє нам писати код вводу-виводу в чисто функціональному порядку.

Виявляється, схема ланцюгових операцій є досить корисною і використовується для безлічі різних речей в Haskell.

Інший приклад - винятки: використовуючи Errorмонаду, операції є ланцюговими таким чином, що вони виконуються послідовно, за винятком випадків, коли помилка кидається, і в цьому випадку решта ланцюга відмовляється.

І синтаксис розуміння списку, і нотація є синтаксичним цукром для ланцюгових операцій за допомогою >>=оператора. Монада - це лише тип, який підтримує >>=оператора.

Приклад 3: Аналізатор

Це дуже простий парсер, який аналізує або цитований рядок, або число:

parseExpr = parseString <|> parseNumber

parseString = do
        char '"'
        x <- many (noneOf "\"")
        char '"'
        return (StringValue x)

parseNumber = do
    num <- many1 digit
    return (NumberValue (read num))

Операції char, digitі т.д. досить прості. Вони або відповідають, або не відповідають. Магія - це монада, яка керує контрольним потоком: Операції виконуються послідовно, поки збіг не завершиться, і в цьому випадку монада повертається до останнього <|>і намагається наступний варіант. Знову спосіб зв’язування операцій з деякою додатковою, корисною семантикою.

Приклад 4: Асинхронне програмування

Наведені вище приклади є в Haskell, але виявляється, F # також підтримує монади. Цей приклад викрадено у « Дон Сайм» :

let AsyncHttp(url:string) =
    async {  let req = WebRequest.Create(url)
             let! rsp = req.GetResponseAsync()
             use stream = rsp.GetResponseStream()
             use reader = new System.IO.StreamReader(stream)
             return reader.ReadToEnd() }

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

У більшості інших мов вам доведеться чітко створити окрему функцію для ліній, що обробляють відповідь. asyncМонада в стані «розкол» блок на своєму і відкласти виконання другої половини. ( async {}Синтаксис вказує, що керуючий потік у блоці визначається asyncмонадою.)

Як вони працюють

Тож як монада зможе виконати всі ці фантазійні контрольно-потокові речі? Те, що насправді відбувається в блоці do (або виразі обчислень, як їх називають у F #), - це те, що кожна операція (в основному кожен рядок) загорнута в окрему анонімну функцію. Потім ці функції поєднуються за допомогою bindоператора (написано >>=в Haskell). Оскільки bindоперація поєднує в собі функції, вона може виконувати їх так, як вважає за потрібне: послідовно, кілька разів, в зворотному порядку, відкинути деякі, виконати деякі на окремій нитці, коли відчуваєш, як це тощо.

Як приклад, це розширена версія IO-коду з прикладу 2:

putStrLn "What is your name?"
>>= (\_ -> getLine)
>>= (\name -> putStrLn ("Welcome, " ++ name ++ "!"))

Це гірше, але також очевидніше, що насправді відбувається. >>=Оператор чарівний інгредієнт: Він приймає значення (на лівій стороні) і комбінує її з функцією (на правій стороні), щоб зробити нове значення. Потім це нове значення приймається наступним >>=оператором і знову поєднується з функцією для отримання нового значення. >>=може розглядатися як міні-оцінювач.

Зауважте, що >>=це перевантажене для різних типів, тому кожна монада має свою реалізацію >>=. (Усі операції в ланцюзі повинні бути типу однієї монади, інакше >>=оператор не працюватиме.)

Найпростіша можлива реалізація >>=просто приймає значення зліва і застосовує його до функції праворуч і повертає результат, але, як було сказано раніше, те, що робить весь шаблон корисним, це коли в монаді реалізується щось додаткове >>=.

Існує деяка додаткова кмітливість у тому, як значення передаються від однієї операції до іншої, але для цього потрібно більш глибоке пояснення системи типу Haskell.

Підводячи підсумки

У Haskell-термінах монада - це параметризований тип, який є екземпляром класу типу Monad, який визначає >>=поряд з кількома іншими операторами. З точки зору мирян, монада - це лише тип, для якого визначена >>=операція.

Сам по собі >>=є просто громіздким способом ланцюгових функцій, але за наявності до-нотації, яка приховує «сантехніку», монадічні операції виявляються дуже приємною і корисною абстракцією, корисною багатьма місцями в мові та корисною для створення власних міні-мов у мові.

Чому монади важкі?

Для багатьох учнів, які навчаються Хаскеллу, монади - це перешкода, на яку вони потрапляють, як цегляна стіна. Справа не в тому, що самі по собі монади є складними, а в тому, що реалізація покладається на багато інших вдосконалених функцій Haskell, таких як параметризовані типи, класи типів тощо. Проблема полягає в тому, що введення-виведення Haskell засноване на монадах, і введення-виведення - це, мабуть, одне з перших, що ви хочете зрозуміти, вивчаючи нову мову - адже створювати програми, які не виробляють жодної програми, не так вже й цікаво. вихід. У мене немає негайного вирішення цієї проблеми з курячим яйцем, окрім трактування вводу / виводу, як "магія відбувається тут", поки у вас не буде достатньо досвіду роботи з іншими частинами мови. Вибачте.

Чудовий блог про монади: http://adit.io/posts/2013-04-17-functors,_applicatives,_and_monads_in_pictures.html


65
Як хтось, хто мав багато проблем із розумінням монардів, можу сказати, що ця відповідь допомогла .. мало. Однак є ще деякі речі, які я не розумію. Яким способом розуміння списку є монадою? Чи є розгорнена форма цього прикладу? Ще одна річ, яка насправді турбує мене з приводу більшості пояснень монади, включаючи це, - це те, що вони продовжують змішуватись "що таке монада?" з "для чого корисна монада?" та "Як реалізується монада?". ви підскочили до цієї акули, коли ви писали "Монада - це в основному лише тип, який підтримує оператора >> =". Що тільки що у мене було ...
Бретон

82
Також я не згоден з вашим висновком щодо того, чому монади важкі. Якщо самі монади не є складними, то ви повинні мати можливість пояснити, що вони бувають без купи багажу. Я не хочу знати про реалізацію, коли я задаю питання "Що таке монада", я хочу знати, що це свербіж, що це означає подряпини. Поки здається, що відповідь така: "Тому що автори haskell є садомазохістами і вирішили, що вам слід зробити щось нерозумно складне для виконання простих речей, тож ви повинні навчитися монадам користуватися haskell, а не тому, що вони будь-яким чином корисні в самі "...
Бретон

69
Але .. це не може бути правильним, чи не так? Я думаю, що монадам важко, тому що ніхто не може зрозуміти, як їх пояснити, не потрапляючи в заплутану інформацію про реалізацію. Я маю на увазі .. що таке шкільний автобус? Це металева платформа з пристроєм спереду, на який витрачається вишуканий нафтопродукт, щоб в циклі керувати металевими поршнями, які, в свою чергу, обертають кривошипний вал, прикріплений до деяких передач, які приводять у рух деякі колеса. Колеса надули навколо себе гумові мішки, які стикуються з поверхнею асфальту, щоб колекція сидінь рухалася вперед. Місця рухаються вперед, тому що ...
Бретон

130
Я читаю все це і досі не знаю, що таке монада, окрім того, що це щось, що програмісти Haskell не розуміють досить добре, щоб пояснити. Приклади не дуже допомагають, враховуючи, що це все, що можна зробити без монад, і ця відповідь не дає зрозуміти, яким чином монади роблять їх легше, лише більш заплутаними. Одна частина цієї відповіді, яка була дуже корисною, полягала в тому, де було видалено синтаксичний цукор прикладу №2. Я кажу, що підійшов близько, тому що, окрім першого рядка, розширення не має жодної реальної подібності до оригіналу.
Лоранс Гонсалвс

81
Ще одна проблема, яка, здається, є ендемічною для пояснень монад, полягає в тому, що це написано в Haskell. Я не кажу, що Haskell - це погана мова - я кажу, що це погана мова для пояснення монад. Якби я знав Хаскелла, я б уже зрозумів монадів, тож якщо ви хочете пояснити монади, почніть з мови, яку люди, які не знають монад, швидше розуміють. Якщо ви повинні використовувати Haskell, взагалі не використовуйте синтаксичний цукор - використовуйте найменший, найпростіший підмножина мови, який ви можете, і не припускайте розуміння Haskell IO.
Лоранс Гонсалвс

712

Пояснення "що таке монада" трохи схоже на те, що сказати "що таке число?" Ми постійно використовуємо цифри. Але уявіть, що ви зустріли когось, хто нічого не знав про цифри. Як чорт ти пояснив, які цифри? І як би ви навіть почали описувати, чому це може бути корисним?

Що таке монада? Коротка відповідь: Це специфічний спосіб з'єднання операцій.

По суті, ви пишете кроки виконання та пов'язуєте їх разом із "функцією прив'язки". (У Haskell це названо >>=.) Ви можете писати дзвінки оператору зв'язування самостійно, або ви можете використовувати синтаксичний цукор, який змушує компілятор вставити ці функції, які викликають вас. Але в будь-якому випадку кожен крок відокремлений викликом цієї функції прив’язки.

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

Це не звучить занадто важко, правда? Але існує кілька видів монади. Чому? Як?

Ну, функція прив'язки може просто взяти результат з одного кроку і подати його на наступний крок. Але якщо це "все", монада робить ... це насправді не дуже корисно. І це важливо для розуміння: Кожна корисна монада робить щось інше, крім того, що є лише монадою. Кожна корисна монада має "особливу силу", що робить її унікальною.

(Монада, яка не робить нічого особливого, називається "монадою ідентичності". Скоріше, як функція ідентичності, це звучить як абсолютно безглузда річ, але, як виявляється, не буває ... Але це вже інша історія ™.)

В основному, кожна монада має власну реалізацію функції зв’язування. І ви можете записати функцію прив'язки таким чином, щоб вона робила перехід між кроками виконання. Наприклад:

  • Якщо кожен крок повертає показник успіху / невдачі, ви можете зв'язати виконання наступного кроку лише в тому випадку, якщо попередній був успішним. Таким чином, невдалий крок припиняє всю послідовність "автоматично", без будь-якого умовного тестування від вас. ( Невдача Монада .)

  • Розширюючи цю ідею, можна реалізувати "винятки". ( Монада помилок або монада винятків .) Оскільки ви визначаєте їх самостійно, а не як мовну особливість, ви можете визначити, як вони працюють. (Наприклад, можливо, ви хочете проігнорувати перші два винятки і скасувати лише тоді, коли буде викинуто третє виключення.)

  • Ви можете зробити кожен крок повернення декількох результатів і мати цикл функції прив’язки над ними, подаючи кожен на наступний крок для вас. Таким чином, вам не доведеться тримати петлі в усьому місці, коли ви маєте справу з кількома результатами. Функція прив'язки "автоматично" робить все, що для вас. ( Список Монада .)

  • Окрім передачі "результату" від одного кроку до іншого, ви можете зв'язати функцію передачі додаткових даних і навколо. Ці дані тепер не відображаються у вашому вихідному коді, але ви все одно можете отримати доступ до нього з будь-якого місця, не вручну передаючи їх до кожної функції. ( Читальник Монада .)

  • Ви можете зробити так, щоб "додаткові дані" можна було замінити. Це дозволяє імітувати руйнівні оновлення , фактично не роблячи деструктивних оновлень. ( Держава Монада та її двоюрідний брат Письменник Монада .)

  • Оскільки ви тільки імітуєте руйнівні оновлення, ви можете тривіально робити те, що було б неможливо з реальними руйнівними оновленнями. Наприклад, ви можете скасувати останнє оновлення або повернути його до більш старої версії .

  • Ви можете зробити монаду, де обчислення можна призупинити , так що ви можете призупинити свою програму, увійти і попрацювати з внутрішніми даними про стан, а потім відновити її.

  • Ви можете реалізувати "продовження" як монада. Це дозволяє зламати розум людей!

Все це та багато іншого можливо з монадами. Звичайно, все це також цілком можливо без монад. Це просто різко простіше використовувати монади.


13
Я ціную вашу відповідь - особливо остаточну поступку, що все це, звичайно, можливо також без монад. Слід зазначити, що з монадами здебільшого простіше, але це часто не так ефективно, як без них. Після того, як вам потрібно буде задіяти трансформатори, додатковий рівень функціональних викликів (та створених функціональних об'єктів) має вартість, яку важко побачити та контролювати, зробивши її розумним синтаксисом невидимим.
seh

1
Принаймні, в Haskell більшу частину накладних витрат монадів оптимізатор позбавляє. Тож єдина реальна "вартість" - це необхідна сила мозку. (Це несуттєво, якщо "ремонтопридатність" - це те, про що ви дбаєте.) Але зазвичай монади роблять все простіше , а не складніше. (Інакше навіщо вам турбуватися?)
MathematicalOrchid

Я не впевнений, підтримує чи ні Haskell це, але математично ви можете визначити монаду або з точки зору >> =, і повернутися, або приєднатися і ap. >> = і повернення - це те, що робить монади практично корисними, але приєднуються та надають більш інтуїтивне розуміння того, що таке монада.
Список Джеремі

15
Виходячи з не математичного, нефункціонального фону програмування, ця відповідь мала найбільш сенс для мене.
jrahhali

10
Це перша відповідь, яка насправді дала мені уявлення про те, що за чорт монада. Дякую, що знайшли спосіб пояснити це!
роботмай

186

Насправді, всупереч загальному розумінню Монад, вони не мають нічого спільного з державою. Монади - це просто спосіб обгортання речей і надання способів робити операції над загорнутою речовиною, не розгортаючи її.

Наприклад, ви можете створити тип, щоб обернути ще один, у Haskell:

data Wrapped a = Wrap a

Для обгортання речі ми визначаємо

return :: a -> Wrapped a
return x = Wrap x

Щоб виконувати операції без розгортання, скажімо, у вас є функція f :: a -> b, тоді ви можете зробити це, щоб зняти цю функцію, щоб діяти на загорнуті значення:

fmap :: (a -> b) -> (Wrapped a -> Wrapped b)
fmap f (Wrap x) = Wrap (f x)

Це про все, що є для розуміння. Однак виявляється, що для цього підйому є більш загальна функція , а саме bind:

bind :: (a -> Wrapped b) -> (Wrapped a -> Wrapped b)
bind f (Wrap x) = f x

bindможе зробити трохи більше fmap, але не навпаки. Власне, fmapможна визначити лише терміни bindі return. Отже, визначаючи монаду .. ви даєте її тип (тут він був Wrapped a), а потім кажете, як працює returnі bindпрацює.

Прикольна річ у тому, що це виявляється такою загальною моделлю, що вона вискакує всюди, а інкапсуляція в чистому вигляді є лише однією з них.

У гарній статті про те, як монади можна використовувати для введення функціональних залежностей і, таким чином, контролювати порядок оцінювання, як це використовується в IO монаді Haskell, перегляньте IO Inside .

Що стосується розуміння монад, не хвилюйтесь над цим. Прочитайте про них те, що вам здається цікавим, і не хвилюйтесь, якщо ви відразу не зрозумієте. Тоді просто пірнати мовою на зразок Haskell. Монади - це одна з таких речей, де розуміючи трюки у мозку на практиці, одного разу ти просто раптом зрозумієш, що ти їх розумієш.


-> це право-асоціативне, дзеркальне відображення функції функції, яке ліво-асоціативне, тому виведення з дужок не має значення.
Маттіас Бенкард

1
Я не думаю, що це зовсім не гарне пояснення. Монади - це просто спосіб? добре, в який бік? Чому б я не інкапсулював, використовуючи клас замість монади?
Бретон

4
@ mb21: Якщо ви просто вказуєте, що дужок занадто багато, зауважте, що a-> b-> c насправді лише короткий для a -> (b-> c). Писати цей конкретний приклад як (a -> b) -> (Ta -> Tb) суворо кажучи лише додаючи зайвих символів, але це морально "правильно", оскільки підкреслює, що fmap відображає функцію типу a -> b до функції типу Ta -> Tb. Спочатку це те, що роблять функтори в теорії категорій, і ось звідки беруться монади.
Микола-К

1
Ця відповідь вводить в оману. Деякі монади взагалі не мають "обгортки", такі функції від фіксованого значення.

1
@DanMandel Monads - це моделі дизайну, які постачають власну обгортку типу даних. Монади розроблені таким чином, щоб абстрактний код котла. Тож, як ви називаєте монаду у своєму коді, вона робить поза кадром речі, про які ви не хочете турбуватися. Подумайте про Nullable <T> або IEnumerable <T>, що вони роблять за кадром? Це Монада.
sksallaj

168

Але, ви могли б винайти Монади!

sigfpe каже:

Але всі вони представляють монади як щось езотеричне, що потребує пояснення. Але я хочу заперечити, що вони взагалі не езотеричні. Насправді, зіткнувшись з різними проблемами функціонального програмування, ви б невблаганно привели до певних рішень, які є прикладом монад. Насправді я сподіваюсь змусити вас їх винайти зараз, якщо ви ще цього не зробили. Тоді невеличкий крок, щоб помітити, що всі ці рішення насправді є маскуванням. І прочитавши це, ви, можливо, зможете зрозуміти інші документи про монади, тому що ви визнаєте все, що бачите, як щось, що ви вже вигадали.

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

У такій імперативній мові програмування, як C ++, функції не ведуть нічого подібного до функцій математики. Наприклад, припустимо, що у нас є функція C ++, яка приймає один аргумент з плаваючою точкою і повертає результат з плаваючою точкою. Поверхово це може здатися трохи схожим на відображення математичної функції, що відповідає дійсності, але функція C ++ може зробити більше, ніж просто повернути число, яке залежить від її аргументів. Він може читати і записувати значення глобальних змінних, а також записувати вихід на екран і отримувати вхід від користувача. Однак чистою функціональною мовою функція може читати лише те, що їй подано в аргументах, і єдиний спосіб, який вона може впливати на світ, - це через значення, які вона повертає.


9
… Найкращий спосіб не тільки в Інтернеті, але і будь-де. (Оригінальний документ Вадлера " Монади функціонального програмування", про який я згадував у своїй відповіді нижче, також не є хорошим.
ShreevatsaR

13
Цей переклад JavaScript повідомлення Sigfpe - це новий найкращий спосіб вивчити монади для людей, які ще не вподобали просунутого Haskell!
Сем Уоткінс

1
Ось як я дізнався, що таке монада. Прогулянка читача через процес винайдення поняття часто є найкращим способом навчання концепції.
Йордан

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

87

Монада - це тип даних, який має дві операції: >>=(ака bind) та return(ака unit). returnприймає довільне значення і створює примірник монади з ним. >>=бере екземпляр монади і відображає функцію над нею. (Ви вже бачите, що монада є дивним видом даних, оскільки в більшості мов програмування ви не можете написати функцію, яка приймає довільне значення і створює з неї тип. Монади використовують певний параметричний поліморфізм .)

У позначеннях Haskell написано інтерфейс monad

class Monad m where
  return :: a -> m a
  (>>=) :: forall a b . m a -> (a -> m b) -> m b

Ці операції повинні підкорятися певним "законам", але це не страшно важливо: "закони" просто кодифікують спосіб поводження розумних реалізацій операцій (в основному, це >>=і returnслід погодитись, як значення трансформуються в монадні екземпляри, і тобто >>=асоціативний).

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

instance Monad [ ] where
    []     >>= k = []
    (x:xs) >>= k = k x ++ (xs >>= k)
    return x     = [x]

instance Monad Maybe where
    Just x  >>= k = k x
    Nothing >>= k = Nothing
    return x      = Just x

де []і :є конструкторами списку, ++є оператором конкатенації, Justі Nothingє Maybeконструкторами. Обидва ці монади містять загальні та корисні схеми обчислень у відповідних типах даних (зауважте, що жоден стосунок не має до побічних ефектів чи вводу / виводу).

Вам дійсно доведеться пограти, щоб написати якийсь нетривіальний код Haskell, щоб зрозуміти, про що монади і чому вони корисні.


Що саме ви маєте на увазі під "відображенням функції над ним"?
Casebash

Casebash, я навмисно неформальний у вступі. Перегляньте приклади наприкінці, щоб зрозуміти, що означає «відображення функції».
Кріс Конвей

3
Монада не є типом даних. Це правило складання функцій: stackoverflow.com/a/37345315/1614973
Дмитро Зайцев

@DmitriZaitsev має рацію, Monads насправді надає власний тип даних, Monads arent типів даних
sksallaj

78

Спершу слід зрозуміти, що таке функтор. Перед цим зрозумійте функції вищого порядку.

Функція вищого порядку - це просто функція, яка приймає функцію як аргумент.

Функтор є будь-який тип конструкції , Tдля якої існує функція вищого порядку, назвемо його map, який перетворює функцію типу a -> b( з урахуванням будь-яких двох типів , aа b) в функцію T a -> T b. Ця mapфункція також повинна підкорятися законам тотожності та композиції таким чином, щоб наступні вирази поверталися істинними для всіх pі q(позначення Haskell):

map id = id
map (p . q) = map p . map q

Наприклад, конструктор типу, який називається, List- це функтор, якщо він оснащений функцією типу, (a -> b) -> List a -> List bяка підкоряється вищезазначеним законам. Очевидна єдина практична реалізація. Отримана List a -> List bфункція повторюється над заданим списком, викликаючи (a -> b)функцію для кожного елемента і повертає список результатів.

Монада по суті тільки функтор Tз двома додатковими методами, join, типу T (T a) -> T a, і unit(іноді називають return, forkабо pure) типу a -> T a. Для списків у Haskell:

join :: [[a]] -> [a]
pure :: a -> [a]

Чому це корисно? Тому що ви могли, наприклад, mapнад списком з функцією, яка повертає список. Joinприймає отриманий список списків і об'єднує їх. Listє монадою, оскільки це можливо.

Ви можете написати функцію , яка робить map, то join. Ця функція називається bind, або flatMap, або (>>=), або (=<<). Зазвичай в Haskell дається екземпляр монади.

Монада повинна задовольняти певним законам, а саме це joinмає бути асоціативним. Це означає, що якщо у вас є значення xтипу, [[[a]]]то воно join (join x)повинно бути рівним join (map join x). І pureмає бути тотожність joinтаким чином, що join (pure x) == x.


3
незначне доповнення до def функції "вищого порядку": вони можуть приймати функції АБО ВЗАЄМО Ось чому вони «вищі», тому що вони роблять справи самі.
Кевін Вон

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

Відео " Брайан Бекман: ​​Не бійся Монади " слідує цій же логіці.
icc97

48

[Відмова від відповідальності: Я все ще намагаюся повністю впорати монади. Далі - лише те, що я зрозумів досі. Якщо це неправильно, сподіваюся, хтось знаючий зателефонує мені на килим.]

Арнар написав:

Монади - це просто спосіб обгортання речей і надання способів робити операції над загорнутою речовиною, не розгортаючи її.

Саме так. Ідея іде так:

  1. Ви берете якусь цінність і обмотуєте її якоюсь додатковою інформацією. Як і значення певного виду (наприклад, ціле число або рядок), так і додаткова інформація має певний вид.

    Наприклад, що додаткова інформація може бути Maybeабо IO.

  2. Тоді у вас є деякі оператори, які дозволяють вам працювати з оберненими даними, переносячи цю додаткову інформацію. Ці оператори використовують додаткову інформацію для того, щоб вирішити, як змінити поведінку операції щодо завершеного значення.

    Наприклад, а Maybe Intможе бути Just Intабо Nothing. Тепер, якщо ви додасте Maybe Intдо Maybe Int, оператор буде перевіряти , якщо вони обидва там Just Intвсередині, і якщо так, то буде розгорнути Intс, передати їм оператор складання, повторно звернути в результаті Intв новий Just Int(який є дійсним Maybe Int), і таким чином повернути a Maybe Int. Але якщо хтось із них був Nothingвсередині, цей оператор просто негайно повернеться Nothing, що знову ж таки є дійсним Maybe Int. Таким чином, ви можете зробити вигляд, що ваші Maybe Ints - це просто звичайні числа і виконувати регулярну математику на них. Якщо ви мали отримати Nothing, ваші рівняння все одно дадуть правильний результат - без того, щоб вам було потрібно перевіряти Nothingвсюди .

Але прикладом є лише те, що відбувається Maybe. Якщо додаткова інформація була an IO, то IOзамість цього буде викликаний спеціальний оператор, визначений для s, і він може зробити щось зовсім інше, перш ніж виконувати додавання. (Гаразд, додавання двох IO Ints разом, мабуть, є безглуздим - я ще не впевнений.) (Якщо ви звернули увагу на Maybeприклад, ви помітили, що "обгортання значення додатковими речами" не завжди правильно. Але важко бути точним, правильним і точним, не будучи недовірливим.)

В принципі, "монада" приблизно означає "зразок" . Але замість книги, наповненої неофіційно поясненими та спеціально названими шаблонами, тепер у вас є мовна конструкція - синтаксис і все, що дозволяє оголошувати нові зразки як речі у вашій програмі . (Тут неточність, що всі зразки мають дотримуватися певної форми, тому монада не настільки загальна, як модель. Але я думаю, що це найближчий термін, який більшість людей знає і розуміє.)

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

Але подумайте про значення синтаксичної підтримки мови для ідеї шаблону: замість того, щоб читати книгу " Банда чотирьох" та запам'ятовувати побудову певного шаблону, ви просто пишете код, який реалізує цю модель в агностиці, загальний шлях один раз, і тоді ви закінчите! Потім ви можете повторно використовувати цей зразок, як-от відвідувач, стратегія, фасад або будь-який інший, просто прикрасивши з ним операції у своєму коді, не потребуючи повторної реалізації його знову і знову!

Тож саме тому люди, які розуміють монадів, вважають їх настільки корисними : це не якась концепція башти зі слонової кістки, яка інтелектуальні сноби пишаються розумінням (гаразд, це теж звичайно, трійник), а насправді робить код простішим.


12
Іноді пояснення від «учня» (як і ви) більше стосується іншого учня, ніж пояснення, яке надходить від експерта. Учні думають однаково :)
Адріан

Що робить щось монадою, це існування функції з типом M (M a) -> M a. Справа в тому, що ви можете перетворити це на одного типу M a -> (a -> M b) -> M b- це робить їх корисними.
Список Джеремі

"монада" приблизно означає "візерунок" ... ні.
Дякую

44

Після великого прагнення я думаю, що нарешті розумію монаду. Після перечитування власної тривалої критики переважної голосової відповіді я запропоную це пояснення.

Є три питання, на які потрібно відповісти, щоб зрозуміти монади:

  1. Для чого потрібна монада?
  2. Що таке монада?
  3. Як реалізується монада?

Як я зазначив у своїх первинних коментарях, занадто багато пояснень монади потрапляють у питання № 3, без, і перш ніж реально адекватно висвітлити питання 2 чи питання 1.

Для чого потрібна монада?

Чисті функціональні мови, такі як Haskell, відрізняються від імперативних мов, таких як C або Java тим, що чиста функціональна програма не обов'язково виконується в певному порядку, один крок за часом. Програма Haskell більше схожа на математичну функцію, в якій ви можете вирішити "рівняння" в будь-якій кількості потенційних замовлень. Це дає ряд переваг, серед яких те, що вона виключає можливість певних видів помилок, зокрема тих, що стосуються таких речей, як "держава".

Однак є певні проблеми, які не так просто вирішити за допомогою цього стилю програмування. Деякі речі, такі як консольне програмування та введення / виведення файлів, потребують того, щоб вони відбувалися у певному порядку або потребували підтримання стану. Один із способів вирішити цю проблему - створити вид об’єкта, який представляє стан обчислення, і ряд функцій, які приймають об'єкт стану як вхідний і повертають новий модифікований об'єкт стану.

Тож давайте створимо гіпотетичне значення "стану", яке представляє стан екранного консолі. як саме будується це значення, не важливо, але скажімо, це масив символів ascii довжини байтів, який представляє те, що зараз видно на екрані, і масив, який представляє останній рядок введення, введений користувачем, у псевдокоді. Ми визначили деякі функції, які приймають стан консолі, змінюють її та повертають новий стан консолі.

consolestate MyConsole = new consolestate;

Отже, щоб виконати програмування консолі, але в чистому функціональному порядку, вам потрібно буде вкласти багато функціональних викликів всередині інших.

consolestate FinalConsole = print(input(print(myconsole, "Hello, what's your name?")),"hello, %inputbuffer%!");

Програмування таким чином зберігає "чистий" функціональний стиль, при цьому змушує зміни консолі відбуватися в певному порядку. Але, ймовірно, ми хочемо зробити більше, ніж просто декілька операцій одночасно, як у наведеному вище прикладі. Функції вкладення таким чином почнуть ставати непридатними. Ми хочемо - це код, який по суті робить те саме, що і вище, але написаний трохи більше, як це:

consolestate FinalConsole = myconsole:
                            print("Hello, what's your name?"):
                            input():
                            print("hello, %inputbuffer%!");

Це справді був би більш зручний спосіб її написання. Як ми це зробимо?

Що таке монада?

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

Як реалізується монада?

Дивіться інші відповіді, які здаються досить вільними, щоб заскочити в деталі цього.


Послідовність не є єдиною причиною визначення монади. Монада - це будь-який функтор, який має зв'язок і повернення. Зв’язування та повернення дають вам послідовність Але вони дають і інші речі. Також зауважте, що ваша улюблена імперативна мова - це фактично модна монада вводу-виводу з класами ОО Полегшити визначення монад означає, що легко використовувати шаблон інтерпретатора - визначте dsl як монаду та інтерпретуйте її!
номен


38

Давши відповідь на це питання кілька років тому, я вважаю, що можу вдосконалити та спростити цю відповідь за допомогою ...

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

У нормальному складі функція - compose (>>)це використання послідовно застосовувати складену функцію до результату попередника. Важливо, що складена функція потрібна для обробки всіх сценаріїв її введення.

(x -> y) >> (y -> z)

Цю конструкцію можна вдосконалити шляхом реструктуризації вкладу, щоб відповідні держави легше опитувались. Отже, замість просто yзначення може стати Mbтаким, як, наприклад, (is_OK, b)якщо воно yвключило поняття дійсності.

Так , наприклад, коли вхід тільки можливо , число, замість того , щоб повертати рядок , яка може містити покірно містити ряд чи ні, ви могли б перебудувати тип в boolвказує на наявність дійсного числа і число в кортежі , такі як, bool * float. Тепер складені функції більше не потребуватимуть розбору вхідного рядка, щоб визначити, чи існує число, але може лише перевірити boolчастину кортежу.

(Ma -> Mb) >> (Mb -> Mc)

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

Однак що робити, якщо ми могли б екстерналізувати зусилля для допиту на ті часи, коли поводження зі сценарієм відбувається звичайно. Наприклад, що якщо наша програма не робить нічого , якщо вхід не в порядку , як в тому, коли is_OKце false. Якщо це було зроблено, тоді складеним функціям не потрібно було б самостійно обробляти цей сценарій, різко спрощуючи свій код і впливаючи на інший рівень повторного використання.

Для досягнення цієї екстерналізації ми могли б використовувати функцію bind (>>=), щоб виконувати compositionзамість цього compose. Таким чином, замість того, щоб просто переносити значення з виводу однієї функції на вхід іншої, Bindслід було б оглянути Mчастину Maта вирішити, чи слід застосовувати складену функцію до a. Звичайно, функція bindбула б визначена спеціально для нашого конкретного M, щоб мати можливість перевіряти її структуру та виконувати будь-який тип додатків, який ми хочемо. Тим не менш, це aможе бути чим завгодно, оскільки bindпросто передає aнесподівану складеній функції, коли вона визначає необхідність застосування. Крім того, самі складені функції більше не потрібно мати справу зMчастина вхідної структури або, спрощуючи їх. Звідси ...

(a -> Mb) >>= (b -> Mc) або більш лаконічно Mb >>= (b -> Mc)

Коротше кажучи, монада екстерналізується і тим самим забезпечує стандартну поведінку навколо обробки певних сценаріїв входу, як тільки вхід стане розробленим для їх достатнього викриття. Ця конструкція є shell and contentмоделлю, де оболонка містить дані, що стосуються застосування складеної функції, і її допитують та залишаються доступними лише bindфункції.

Тому монада - це три речі:

  1. Mоболонка для проведення монади відповідної інформації,
  2. bindфункції реалізовані , щоб використовувати цю інформацію оболонки в її застосуванні в складі функцій до вартості контента (и) , які він знаходить всередині оболонки, і
  3. композиційні функції форми, що a -> Mbстворюють результати, що включають дані монадичного управління.

Взагалі кажучи, вхід до функції є набагато обмежуючим, ніж її вихід, який може включати такі речі, як умови помилок; отже, Mbструктура результатів, як правило, дуже корисна. Наприклад, оператор ділення не повертає число, коли дільник 0.

Крім того, monads може включати функції обтікання, які обертають значення, aу тип монадики Maта загальні функції a -> b, в монадійні функції a -> Mb, шляхом обгортання їх результатів після застосування. Звичайно, як bind, наприклад, такі функції обгортання специфічні для M. Приклад:

let return a = [a]
let lift f a = return (f a)

Конструкція bindфункції передбачає незмінні структури даних та чисті функції інших речей ускладнюються і гарантій не можна давати. Таким чином, існують монадійні закони:

Дано ...

M_ 
return = (a -> Ma)
f = (a -> Mb)
g = (b -> Mc)

Тоді...

Left Identity  : (return a) >>= f === f a
Right Identity : Ma >>= return    === Ma
Associative    : Ma >>= (f >>= g) === Ma >>= ((fun x -> f x) >>= g)

Associativityозначає, що bindзберігається порядок оцінювання незалежно від того, коли bindзастосовується. Тобто, у визначенні Associativityвище, в силу ранньої оцінки з дужки bindingз fі gпризведе лише до функції , яка очікує Maв цілях завершення bind. Отже, оцінювання Maмає бути визначене до того, як його значення може бути застосовано fі до цього результат, у свою чергу, застосований g.


"... але сподіваюся, що інші вважають це корисним", воно було для мене дійсно корисним, незважаючи на всі наголошені речення: D

Це найбільш стисле і чітке пояснення монад, які я коли-небудь читав / дивився / чув. Дякую!
Джеймс

Існує важлива різниця між Монадою і Моноїдом. Монада - це правило "складати" функції між різними типами, тому вони не утворюють бінарних операцій, як це потрібно для Monoids, див. Тут докладніше: stackoverflow.com/questions/2704652/…
Дмитро Зайцев

Так. Ви праві. Ваша стаття була над моєю головою :). Однак я вважав це лікування дуже корисним (і додав його до моїх як вказівку для інших). Дякую головам вгору: stackoverflow.com/a/7829607/1612190
Джордж

2
Ви могли б плутати теорію груп алгебраїчної з теорією категорій , де Монада звідки. Перша - теорія алгебраїчних груп, яка не пов'язана між собою.
Дмитро Зайцев

37

Фактично, монада є формою "оператора типу". Це зробить три речі. Спочатку воно "загортає" (або іншим чином перетворює) значення одного типу в інший тип (зазвичай його називають "монадичним типом"). По-друге, це зробить усі операції (або функції) доступними для базового типу, доступними для монадичного типу. Нарешті, він надасть підтримку для поєднання свого самоврядування з іншою монадою для отримання складеної монади.

"Монада", можливо, є еквівалентом "нульових типів" у Visual Basic / C #. Він приймає нерегульований тип "T" і перетворює його в "Nullable <T>", а потім визначає, що означають усі бінарні оператори під Nullable <T>.

Побічні ефекти представлені одночасно. Створюється структура, яка містить описи побічних ефектів поряд із зворотним значенням функції. Потім "підняті" операції копіюють навколо побічних ефектів, коли значення передаються між функціями.

Їх називають "монадами", а не легшими для розуміння назвою "операторів типу" з кількох причин:

  1. Монади мають обмеження щодо того, що вони можуть робити (детальніше див. Дефінітон).
  2. Ці обмеження, поряд із тим, що задіяні три операції, відповідають структурі того, що називається монадою в Теорії категорій, що є малозрозумілою галуззю математики.
  3. Вони були розроблені прихильниками "чистих" функціональних мов
  4. Прихильники чистих функціональних мов, як незрозумілі галузі математики
  5. Оскільки математика незрозуміла, а монади пов'язані з певними стилями програмування, люди, як правило, використовують слово монада як якесь таємне рукостискання. Через це ніхто не потрудився вкладати гроші в краще ім’я.

1
Монади не були "розроблені", вони застосовувалися від однієї області (теорія категорій) до іншої (введення / виведення в суто функціональних мовах програмування). Хіба Ньютон "спроектував" обчислення?
Джаред Updike

1
Пункти 1 та 2 вище є правильними та корисними. Бали 4 і 5 є своєрідними hominem, навіть якщо це більш-менш вірно. Вони не дуже допомагають пояснити монадам.
Jared Updike

13
Re: 4, 5: річ "Таємне рукостискання" - це червона оселедець. Програмування наповнене жаргоном. Haskell просто трапляється називати речі такими, якими вони є, не роблячи виду, що щось знову відкривають. Якщо воно вже існує в математиці, навіщо складати для нього нову назву? Назва насправді не є причиною того, що люди не отримують монадів; вони - тонка концепція. Пересічна людина, ймовірно, розуміє додавання та множення, чому вони не отримують поняття абелевої групи? Тому що це більш абстрактно і загально, і ця людина не виконала роботи, щоб обернути голову навколо концепції. Зміна імені не допоможе.
Jared Updike

16
Зітхання ... Я не роблю напад на Хаскелла ... Я жартував. Тож я насправді не зважаю на те, щоб бути "ad hominem". Так, обчислення було "спроектовано". Ось чому, наприклад, студентів з обчисленням навчають нотації Лейбніца, а не прискіпливих речей, які використовує Netwton. Кращий дизайн. Хороші імена багато допомагають зрозуміти. Якщо я назвав абелеві групи "роздутими стручками від зморшок", у вас можуть виникнути проблеми з розумінням мене. Ви можете сказати "але це ім'я - це нісенітниця", ніхто б їх ніколи так не називав. Для людей, які ніколи не чули про теорію категорій, "монада" звучить як нісенітниця.
Скотт Вісневський

4
@Scott: вибачте, якщо в моїх обширних коментарях здавалося, що я захищаю Хаскелла. Я насолоджуюсь вашим гумором щодо таємного рукостискання, і ви зауважте, я сказав, що це більш-менш правда. :-) Якби ви назвали абелеві групи "розмеленими стручками проти зморшок", ви б зробили ту саму помилку, намагаючись надати монадам "кращу назву" (пор. F # "вирази обчислень"): термін існує, і люди, які переймаються, знають, що таке монади є, але не те, що таке "теплі нечіткі речі" (або "вирази обчислень"). Якщо я розумію, що ви правильно використовуєте термін "оператор типу", існує безліч операторів іншого типу, ніж монади.
Jared Updike

35

(Див. Також відповіді у розділі Що таке монада? )

Хорошою мотивацією для монадів є сигфпе (Дан Піпоні), з яким ти міг би винайти монади! (І, можливо, Ви вже є) . Існує багато інших навчальних посібників з монади , багато з яких помилково намагаються пояснити монади «простими термінами», використовуючи різні аналогії: це помилка підручника монади ; уникати їх.

Як говорить ДР Маківер у " Розкажіть нам, чому ваша мова смокче :"

Отже, речі, які я ненавиджу щодо Haskell:

Почнемо з очевидного. Підручники Монади. Ні, не монади. Зокрема навчальні посібники. Вони нескінченні, роздуті і шановний бог, вони нудні. Далі я ніколи не бачив переконливих доказів того, що вони насправді допомагають. Прочитайте визначення класу, напишіть якийсь код, здолайте страшну назву.

Ви кажете, що розумієте монаду "Можливо"? Добре, ти на своєму шляху. Просто почніть використовувати інші монади і рано чи пізно ви зрозумієте, що таке монади взагалі.

[Якщо ви орієнтовані на математику, ви, можливо, захочете проігнорувати десятки навчальних посібників і вивчити їх визначення, або дотримуватися лекцій з теорії категорій :) Основна частина визначення полягає в тому, що Monad M включає "конструктор типу", який визначається для кожного існуючий тип "T" нового типу "MT", а також деякі способи переходу вперед і назад між "звичайними" типами і типами "М".]

Крім того, напрочуд, одне з найкращих вступів до монад - це фактично одна з ранніх наукових праць, що представляють монади, « Монади Філіпа Вадлера» для функціонального програмування . Насправді є практичні, нетривіальні мотивуючі приклади, на відміну від багатьох штучних навчальних посібників там.


2
Єдиною проблемою у роботі Вадлера є те, що нотація різна, але я погоджуюся з тим, що документ є досить переконливим і чіткою, стислою мотивацією застосування монад.
Jared Updike

+1 для "помилки підручника з монадою". Навчальні посібники щодо монад схожі на те, що кілька навчальних посібників намагаються пояснити поняття цілих чисел. В одному підручнику сказано: «1 схожий на яблуко»; в іншому підручнику сказано: «2 - це як груша»; третя говорить: "3 - це помаранчевий". Але ви ніколи не отримуєте всієї картини з жодного навчального посібника. Що я взяв із цього, це те, що монади - це абстрактне поняття, яке можна використовувати для самих різних цілей.
stakx - більше не вносить свій внесок

@stakx: Так, правда. Але я не мав на увазі, що монади - це абстракція, якої ти не можеш навчитися чи не повинен навчитися; лише те, що найкраще дізнатися це після того, як ви побачили достатньо конкретних прикладів для сприйняття однієї основної абстракції. Дивіться мою іншу відповідь тут .
ShreevatsaR

5
Іноді я відчуваю, що існує так багато навчальних посібників, які намагаються переконати читача, що монади корисні, використовуючи код, який робить складні чи корисні речі. Це місяцями гальмувало моє розуміння. Я не вчуся так. Я вважаю за краще бачити надзвичайно простий код, роблячи щось дурне, що можу подумки пройти, і я не міг знайти такий приклад. Я не можу дізнатися, чи є перший приклад монадою для розбору складної граматики. Я можу дізнатися, чи є монадою підсумовувати цілі числа.
Рафаель С. Кальсаверіні

Згадка лише конструктора типу неповна: stackoverflow.com/a/37345315/1614973
Дмитро Зайцев

23

Монади повинні контролювати потоки, які абстрактні типи даних відносяться до даних.

Іншими словами, багатьом розробникам зручно використовувати ідеї "Набори", "Списки", "Словники" (або "Хеші" або "Карти") і Дерева. У межах цих типів даних є багато спеціальних випадків (наприклад, InsertionOrderPreservingIdentityHashMap).

Однак, зіткнувшись з програмою «потік», багато розробників не зазнали багатьох інших конструкцій, ніж якщо, перемикання / випадок, робити, поки закриття goto (grr) та (можливо).

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

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

Наприклад, монада "якщо":

if( clause ) then block

у найпростішому є два слоти - і клаузу, і блок. ifМонада, як правило , побудовані , щоб оцінити результат статті, і якщо не помилково, оцінювати блок. Багато розробників не знайомі з монадами, коли вони навчаються "якщо", і просто не потрібно розуміти монадів, щоб писати ефективну логіку.

Монади можуть ускладнитися, так само, як структури даних можуть ускладнитися, але існує багато широких категорій монад, які можуть мати схожу семантику, але різної реалізації та синтаксису.

Звичайно, таким же чином, як структури даних можуть бути повторені або пройдені, монади можуть бути оцінені.

Компілятори можуть мати або не мати підтримку визначених користувачем монад. Haskell, безумовно, робить. Ioke має деякі подібні можливості, хоча в мові не використовується термін монада.


14

Мій улюблений підручник Монада:

http://www.haskell.org/haskellwiki/All_ About_Monads

(із 170 000 звернень у пошуку Google для "монада"!)

@Stu: Суть монад полягає в тому, щоб додати (як правило) послідовну семантику до чистого коду; Ви навіть можете скласти монади (використовуючи Monad Transformers) та отримати більш цікаву та складну комбіновану семантику, наприклад, розбір з керуванням помилками, спільним станом та веденням журналу, наприклад. Все це можливо в чистому коді, монади просто дозволяють абстрагувати його і використовувати його в модульних бібліотеках (завжди добре в програмуванні), а також надають зручний синтаксис, щоб він виглядав обов'язковим.

У Haskell вже є перевантаження оператора [1]: він використовує класи типів так само, як можна використовувати інтерфейси в Java або C #, але Haskell просто трапляється, щоб дозволити також не алфавітно-цифрові лексеми, як + && і>, як ідентифікатори інфіксації. Це лише перевантаження оператора, як ви дивитесь на це, якщо ви маєте на увазі "перевантаження крапки з комою" [2]. Це звучить як чорна магія і вимагає неприємностей "перевантажити крапку з комою" (малюнок заповзятливих хакерів Perl отримує вітром цієї ідеї), але справа в тому, що без монад не існує крапки з комою, оскільки суто функціональний код не вимагає і не дозволяє явного послідовності.

Це все звучить набагато складніше, ніж потрібно. Стаття sigfpe досить класна, але використовує Haskell, щоб пояснити це, який тип не в змозі розірвати проблему з куркою і яйцями, коли зрозуміти Haskell для того, щоб врізати Monads і зрозуміти Monads, щоб впорати Haskell.

[1] Це окрема проблема, ніж монади, але монади використовують функцію перевантаження оператора Haskell.

[2] Це також надмірне спрощення, оскільки оператор для ланцюжка монадичних дій є >> = (вимовляється "прив'язувати"), але є синтаксичний цукор ("робити"), який дозволяє використовувати дужки та крапки з комою та / або відступ та нові рядки.


9

Останнім часом я по-іншому думаю про Монад. Я розглядав їх як математичне абстрагування порядку виконання , що робить можливим нові види поліморфізму.

Якщо ви використовуєте імперативну мову, і ви пишете деякі вирази по порядку, код ЗАВЖДИ працює саме в тому порядку.

І в простому випадку, коли ви використовуєте монаду, вона відчуває те саме - ви визначаєте список виразів, які відбуваються по порядку. За винятком того, що залежно від того, яку монаду ви використовуєте, ваш код може працювати в порядку (наприклад, в монаді IO), паралельно над декількома елементами одночасно (як у монаді списку), він може зупинитися наскрізь (наприклад, у монаді "Можливо") , вона може призупинити початок роботи, щоб відновитись пізніше (як у монаді відновлення), вона може перемотатися і розпочатись спочатку (як у монаді транзакцій), або може перемотати частину, щоб спробувати інші параметри (наприклад, у логіці монади) .

Оскільки монади є поліморфними, можна запускати один і той же код у різних монах, залежно від ваших потреб.

Крім того, в деяких випадках можливо поєднувати монади разом (з монадними трансформаторами), щоб отримати кілька функцій одночасно.


9

Я все ще новачок у монадах, але я подумав, що поділюсь посиланням, на який я знайшов, що мені дуже добре читати (З СИСТЕМИ !!): http://www.matusiak.eu/numerodix/blog/2012/3/11/ монади для непрофесійного / (без приналежності)

В основному, тепла і нечітка концепція, яку я отримала зі статті, - це концепція того, що монади - це в основному адаптери, які дозволяють розрізненим функціям працювати компонованим способом, тобто вміти поєднувати декілька функцій і змішувати їх і співставляти, не турбуючись про непослідовне повернення типи тощо. Тож функція BIND відповідає за збереження яблук з яблуками, а апельсини - з апельсинами, коли ми намагаємося зробити ці адаптери. А функція LIFT відповідає за прийняття функцій "нижчого рівня" та "модернізацію" їх для роботи з BIND-функціями, а також для їх компонування.

Я сподіваюся, що я зрозумів це правильно, і що ще важливіше, сподіваюся, що стаття має дійсне уявлення про монади. Якщо нічого іншого, ця стаття допомогла зіткнутися з моїм апетитом дізнатися більше про монади.


Приклади пітона полегшили розуміння! Дякую, що поділились.
Райан Ефенді

8

На додаток до чудових відповідей, наведених вище, дозвольте запропонувати вам посилання на наступну статтю (Патрік Томсон), яка пояснює монади, пов’язавши концепцію з бібліотекою JavaScript jQuery (та спосіб використання "ланцюжків методів" для маніпулювання DOM) : jQuery - це монада

Сама документація jQuery не посилається на термін "монада", але говорить про "модель побудови", яка, мабуть, більш знайома. Це не змінює того факту, що у вас є належна монада, можливо, навіть не усвідомлюючи цього.


Якщо ви використовуєте jQuery, це пояснення може бути дуже корисним, особливо якщо ваш Haskell не сильний
byteclub

10
JQuery очевидно не монада. Пов'язана стаття неправильна.
Тоні Морріс

1
Бути "рішучим" не дуже переконливо. Деякі корисні дискусії з цієї теми див.
IsJQuery a monad

1
Дивіться також Дуглас Крекфорд у Google Talk Monads and Gonads та його код Javascript для виконання мод, розширюючи аналогічну поведінку бібліотек AJAX та Обіцянь: douglascrockford /
monad


7

Монада - це спосіб поєднання обчислень, які мають спільний контекст. Це як побудова мережі труб. При побудові мережі, через неї не протікають дані. Але коли я закінчую розбивати всі біти разом із «зв'язувати» та «повертати», я викликаю щось подібне, runMyMonad monad dataі дані протікають по трубах.


1
Це більше схоже на додаток, ніж Монада. Завдяки Monads вам потрібно отримати дані з труб, перш ніж ви зможете вибрати наступну трубу для підключення.
Пік

так, ви описуєте додаток, а не монаду. Монада - це будувати наступний відрізок труби на місці, залежно від даних, які досягли цієї точки, всередині труби.
Буде Несс

6

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


5

Якщо я правильно зрозумів, IEnumerable походить від монад. Цікаво, чи може це бути цікавим кутом підходу для нас із світу C #?

Для чого це варто, ось кілька посилань на підручники, які мені допомогли (і ні, я досі не зрозумів, що таке монади).


5

Дві речі, які мені найкраще допомогли, дізнавшись про це:

Глава 8, "Функціональні парсери", з книги Програмування Грема Хаттона в Haskell . Насправді це взагалі не згадує про монади, але якщо ви зможете опрацювати розділ і реально зрозуміти все, що в ньому, особливо як оцінюється послідовність операцій зв’язування, ви зрозумієте внутрішні монади. Очікуйте, що це зробить кілька спроб.

Підручник Все про монади . Це дає кілька хороших прикладів їх використання, і я повинен сказати, що аналогія в Додатку я працювала на мене.


5

Моноїд видається чимось таким, що забезпечує те, що всі операції, визначені на моноїді та підтримуваному типі, завжди повертатимуть підтримуваний тип усередині Monoid. Наприклад, будь-яке число + будь-яке число = число, помилок немає.

Тоді як поділ приймає два дроби і повертає дробовий, який визначає поділ на нуль як Нескінченність у haskell somewhy (що трапляється частково частинним) ...

У будь-якому випадку, видається, Monads - це лише спосіб переконатися, що ваша ланцюжок операцій поводиться передбачувано, а функція, яка претендує на Num -> Num, складена з іншою функцією Num-> Num, що називається x скажімо, вогнем ракет.

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

У Haskell основний тип IO () або IO [()], дистрибуція дивна, і я не буду це обговорювати, але ось що я думаю:

Якщо у мене є головне, я хочу, щоб він здійснив ланцюжок дій, тому я запускаю програму, щоб створити ефект - як правило, через IO. Таким чином, я можу з'єднати операції вводу-виводу в основному разом, щоб зробити IO, більше нічого.

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

В основному, Monads, здається, є підказкою для компілятора, що "гей, ви знаєте, що ця функція повертає число тут, вона насправді не завжди працює. Іноді може створювати число, а іноді і зовсім нічого, просто тримати це в розум ". Знаючи це, якщо ви намагаєтеся стверджувати монадійну дію, монадійна дія може виступати як виняток із компіляції часу, кажучи "ей, це насправді не число, це МОЖНА бути числом, але ви не можете цього припустити, зробіть щось" щоб забезпечити прийнятність потоку ". що запобігає непередбачуваній поведінці програми - в достатній мірі.

Здається, монади не стосуються чистоти чи контролю, а збереження ідентичності категорії, за якою будь-яка поведінка передбачувана і визначена, або не складається. Ви не можете нічого робити, коли від вас очікується щось зробити, і ви не можете зробити щось, якщо від вас нічого не робити (видно).

Найбільшою причиною, про яку я міг придумати Monads, є - перегляньте Процедурний / OOP-код, і ви помітите, що ви не знаєте, звідки починається програма і не закінчується. Все, що ви бачите, - це багато стрибків і багато математики , магія та ракети. Ви не зможете його підтримувати, і якщо зможете, ви витратите досить багато часу, обертаючи свою думку навколо всієї програми, перш ніж зможете зрозуміти будь-яку її частину, адже модульність у цьому контексті базується на взаємозалежних "розділах" коду, де код оптимізований таким чином, щоб бути максимально пов'язаним для обіцянки ефективності / взаємозв'язку. Монади є дуже конкретними і чітко визначеними за визначенням, і забезпечують можливість протікання програми аналізувати та виділяти частини, які важко проаналізувати - оскільки вони самі є монадами. Монада видається " або знищити Всесвіт, або навіть спотворити час - ми не маємо уявлення і не маємо жодних гарантій того, що це таке. Монада гарантує, що це таке, що це. що дуже потужно. або знищити Всесвіт, або навіть спотворити час - ми не маємо уявлення і не маємо жодних гарантій того, що це таке. Монада гарантує, що це таке, що це. що дуже потужно.

Усі речі в "реальному світі" видаються монадами, в тому сенсі, що вони пов'язані певними законами, що дотримуються, що запобігають плутанині. Це не означає, що ми повинні імітувати всі операції цього об’єкта для створення класів, натомість ми можемо просто сказати «квадрат - це квадрат», нічого, крім квадрата, навіть прямокутника чи кола, і «квадрат має площу довжини одного з його існуючих розмірів, помножених на себе. Незалежно від того, який квадрат у вас є, якщо це квадрат у двовимірному просторі, це площа абсолютно не може бути нічого, крім довжини у квадраті, це майже тривіально довести. Це дуже потужно, оскільки нам не потрібно робити твердження, щоб переконатися, що наш світ такий, який він є, ми просто використовуємо наслідки реальності, щоб не допустити, щоб наші програми випали з колії.

Я майже гарантовано помиляюся, але я думаю, що це може допомогти комусь там, тому, сподіваємось, це комусь допоможе.


5

У контексті Scala ви знайдете наступне, що є найпростішим визначенням. В основному flatMap (або зв'язує) є "асоціативним" і існує ідентичність.

trait M[+A] {
  def flatMap[B](f: A => M[B]): M[B] // AKA bind

  // Pseudo Meta Code
  def isValidMonad: Boolean = {
    // for every parameter the following holds
    def isAssociativeOn[X, Y, Z](x: M[X], f: X => M[Y], g: Y => M[Z]): Boolean =
      x.flatMap(f).flatMap(g) == x.flatMap(f(_).flatMap(g))

    // for every parameter X and x, there exists an id
    // such that the following holds
    def isAnIdentity[X](x: M[X], id: X => M[X]): Boolean =
      x.flatMap(id) == x
  }
}

Напр

// These could be any functions
val f: Int => Option[String] = number => if (number == 7) Some("hello") else None
val g: String => Option[Double] = string => Some(3.14)

// Observe these are identical. Since Option is a Monad 
// they will always be identical no matter what the functions are
scala> Some(7).flatMap(f).flatMap(g)
res211: Option[Double] = Some(3.14)

scala> Some(7).flatMap(f(_).flatMap(g))
res212: Option[Double] = Some(3.14)


// As Option is a Monad, there exists an identity:
val id: Int => Option[Int] = x => Some(x)

// Observe these are identical
scala> Some(7).flatMap(id)
res213: Option[Int] = Some(7)

scala> Some(7)
res214: Some[Int] = Some(7)

ПРИМІТКА Строго кажучи, що визначення монади у функціональному програмуванні не те саме, що визначення монади в теорії категорій , яке визначається по черзі mapта flatten. Хоча вони є певним еквівалентом за певних відображень. Ця презентація дуже хороша: http://www.slideshare.net/samthemonad/monad-presentation-scala-as-a-category


5

Ця відповідь починається з мотивуючого прикладу, працює через приклад, виводить приклад монади і формально визначає "монаду".

Розглянемо ці три функції в псевдокоді:

f(<x, messages>) := <x, messages "called f. ">
g(<x, messages>) := <x, messages "called g. ">
wrap(x)          := <x, "">

fприймає впорядковану пару форми <x, messages>і повертає впорядковану пару. Він залишає перший елемент недоторканим і додає "called f. "до другого. Те саме з g.

Ви можете скласти ці функції та отримати своє початкове значення разом із рядком, який показує, у якому порядку виклики функцій:

  f(g(wrap(x)))
= f(g(<x, "">))
= f(<x, "called g. ">)
= <x, "called g. called f. ">

Вам не подобається той факт , що fі gнесуть відповідальність за додавання своїх власних повідомлень журналу попередньої інформації про реєстрацію. (Просто уявіть заради аргументу, що замість додавання рядків, fа також gповинен виконувати складну логіку на другому пункті пари. Було б болю повторити цю складну логіку у двох - і більше - різних функціях.)

Ви вважаєте за краще писати простіші функції:

f(x)    := <x, "called f. ">
g(x)    := <x, "called g. ">
wrap(x) := <x, "">

Але подивіться, що станеться, коли ви їх складете:

  f(g(wrap(x)))
= f(g(<x, "">))
= f(<<x, "">, "called g. ">)
= <<<x, "">, "called g. ">, "called f. ">

Проблема полягає в тому, що переведення пари у функцію не дає тобі, що ти хочеш. Але що робити, якщо ви могли подати пару у функцію:

  feed(f, feed(g, wrap(x)))
= feed(f, feed(g, <x, "">))
= feed(f, <x, "called g. ">)
= <x, "called g. called f. ">

Читати feed(f, m)як "подавати mв f". Для того, щоб нагодувати пару <x, messages>в функцію f, щоб пройти x в f, отримати <y, message>з f, і повернутися <y, messages message>.

feed(f, <x, messages>) := let <y, message> = f(x)
                          in  <y, messages message>

Зверніть увагу на те, що відбувається, коли ви робите три функції зі своїми функціями:

По-перше: якщо ви перегорнете значення, а потім подайте отриману пару у функцію:

  feed(f, wrap(x))
= feed(f, <x, "">)
= let <y, message> = f(x)
  in  <y, "" message>
= let <y, message> = <x, "called f. ">
  in  <y, "" message>
= <x, "" "called f. ">
= <x, "called f. ">
= f(x)

Це те саме, що передавати значення функції.

Друге: якщо ви годуєте пару в wrap:

  feed(wrap, <x, messages>)
= let <y, message> = wrap(x)
  in  <y, messages message>
= let <y, message> = <x, "">
  in  <y, messages message>
= <x, messages "">
= <x, messages>

Це не змінює пари.

Третє: якщо ви визначаєте функцію , яка приймає xі подає g(x)в f:

h(x) := feed(f, g(x))

і подати в нього пару:

  feed(h, <x, messages>)
= let <y, message> = h(x)
  in  <y, messages message>
= let <y, message> = feed(f, g(x))
  in  <y, messages message>
= let <y, message> = feed(f, <x, "called g. ">)
  in  <y, messages message>
= let <y, message> = let <z, msg> = f(x)
                     in  <z, "called g. " msg>
  in <y, messages message>
= let <y, message> = let <z, msg> = <x, "called f. ">
                     in  <z, "called g. " msg>
  in <y, messages message>
= let <y, message> = <x, "called g. " "called f. ">
  in <y, messages message>
= <x, messages "called g. " "called f. ">
= feed(f, <x, messages "called g. ">)
= feed(f, feed(g, <x, messages>))

Це те саме, що подавати пару gі вводити отриману пару в f.

У вас більша частина монади. Тепер вам просто потрібно знати про типи даних у вашій програмі.

Що таке значення <x, "called f. ">? Ну, це залежить від типу типу значення x. Якщо xтип t, то для вашої пари є значення типу "пара tі рядок". Зателефонуйте цьому типу M t.

Mє конструктором типу: Mпоодинці не посилається на тип, а M _посилається на тип, як тільки ви заповнюєте порожню типом. An M int- пара int та string. А M string- пара рядків і рядків. І т.д.

Вітаємо, ви створили монаду!

Формально ваша монада - кортеж <M, feed, wrap>.

Монада - це кортеж, <M, feed, wrap>де:

  • M - конструктор типу.
  • feedприймає (функція, яка приймає a tі повертає M u) і an M tі повертає an M u.
  • wrapбере vі повертає M v.

t, uі vбудь-які три типи, які можуть бути або не бути однаковими. Монада задовольняє трьом властивостям, які ви довели для своєї конкретної монади:

  • Подача загорнутого tу функцію - це те саме, що передача розгорнутої tфункції.

    Формально: feed(f, wrap(x)) = f(x)

  • Годування M tв wrapкомерційне підприємство не робить нічого M t.

    Формально: feed(wrap, m) = m

  • Подання M t(викликати його m) функції, яка

    • переходить tвg
    • отримує M u(називайте його n) відg
    • подає nвf

    те саме, що

    • годування mвg
    • отримання nвідg
    • годування nвf

    Формально: feed(h, m) = feed(f, feed(g, m))кудиh(x) := feed(f, g(x))

Як правило, feedназивається bind(AKA >>=в Haskell) і wrapназивається return.


5

Я спробую пояснити Monadв контексті Haskell.

У функціональному програмуванні важливе значення має склад функцій. Це дозволяє нашій програмі складатися з невеликих, легко читаються функцій.

Скажімо, у нас є дві функції: g :: Int -> Stringі f :: String -> Bool.

Ми можемо зробити (f . g) x, що точно так само f (g x), xяк і Intзначення.

При складанні / застосуванні результату однієї функції до іншої важливо поєднання типів. У вищенаведеному випадку тип повернутого результату gповинен бути таким же, як і тип, прийнятий f.

Але іноді значення знаходяться в контекстах, і це робить трохи менш простим вирівнювання типів. (Значення в контекстах дуже корисно. Наприклад, Maybe Intтип представляє Intзначення, яке може бути там, IO Stringтип представляє Stringзначення, яке є в результаті виконання деяких побічних ефектів.)

Скажімо, у нас зараз є g1 :: Int -> Maybe Stringі f1 :: String -> Maybe Bool. g1і f1дуже схожі на gі fвідповідно.

Ми не можемо зробити (f1 . g1) xабо f1 (g1 x), де xце Intзначення. Тип результату, який повертається, g1- це не те, що f1очікує.

Ми могли скласти fі gз .оператором, але зараз ми не можемо складати f1і g1з .. Проблема полягає в тому, що ми не можемо прямо передати значення в контексті функції, яка очікує значення, яке не знаходиться в контексті.

Не було б непогано, якщо ми запровадимо оператора для складання g1та f1, такого, щоб ми могли писати (f1 OPERATOR g1) x? g1повертає значення в контексті. Значення буде виведено з контексту та застосовано до f1. І так, у нас такий оператор. Це <=<.

У нас також є >>=оператор, який робить для нас абсолютно те саме, щоправда, у дещо іншому синтаксисі.

Ми пишемо: g1 x >>= f1. g1 xє Maybe Intцінністю. >>=Оператор допомагає прийняти це Intзначення з «можливо, не-там» контексту, і застосувати його до f1. Результат f1, який є Maybe Bool, буде результатом всієї >>=операції.

І нарешті, навіщо Monadкорисно? Оскільки Monadклас типу визначає >>=оператора, майже такий самий, як Eqклас типу, який визначає оператори ==та /=.

На закінчення, Monadклас типу визначає >>=оператор, який дозволяє нам передавати значення в контексті (ми називаємо ці монадичні значення) функціям, які не очікують значень у контексті. Буде опікуватися контекстом.

Якщо тут слід пам’ятати одну річ, це те, Monadщо дозволити склад функції, що включає значення в контекстах .



IOW, Monad є узагальненим протоколом виклику функції.
Буде Несс

Ви відповідаєте, на мою думку, є найбільш корисним. Хоча я мушу сказати, що, на мою думку, потрібно робити акцент на тому, що функції, які ви переглядаєте, не включають значення лише в контексти, вони активно ставлять значення в контексти. Так, наприклад, функція, f :: ma -> mb дуже легко складеться з іншою функцією, g :: mb -> m c. Але монади (конкретно прив'язуються) дозволяють нам постійно складати функції, які ставлять свій внесок у той самий контекст, без того, щоб нам потрібно було спочатку виводити значення з цього контексту (що фактично би вилучило інформацію зі значення)
Джеймс

@James Я вважаю, що це має бути акцентом для функторів?
Йонас

@Jonas Я думаю, я не пояснив це. Коли я кажу, що функції ставлять значення в контексти, я маю на увазі, що вони мають тип (a -> mb). Вони дуже корисні, оскільки введення значення в контекст додає до нього нову інформацію, але зазвичай це буде складно ланцюг a (a -> mb) та a (b -> mc), оскільки ми не можемо просто вийняти значення контексту. Тож нам би довелося використовувати якийсь згорнутий процес, щоб об'єднати ці функції в розумний спосіб залежно від конкретного контексту, і монади просто дозволяють нам це робити послідовно, незалежно від контексту.
Джеймс

5

тл; д-р

{-# LANGUAGE InstanceSigs #-}

newtype Id t = Id t

instance Monad Id where
   return :: t -> Id t
   return = Id

   (=<<) :: (a -> Id b) -> Id a -> Id b
   f =<< (Id x) = f x

Пролог

Оператор $програми функцій

forall a b. a -> b

є канонічно визначеним

($) :: (a -> b) -> a -> b
f $ x = f x

infixr 0 $

з точки зору застосування примітивної функції Haskell f x( infixl 10).

Склад .визначається в термінах $як

(.) :: (b -> c) -> (a -> b) -> (a -> c)
f . g = \ x -> f $ g x

infixr 9 .

і задовольняє еквівалентності forall f g h.

     f . id  =  f            :: c -> d   Right identity
     id . g  =  g            :: b -> c   Left identity
(f . g) . h  =  f . (g . h)  :: a -> d   Associativity

.є асоціативним і idє його правою та лівою тотожністю.

Клейслі потрійний

У програмуванні монада - це конструктор типу функтора з екземпляром класу типу монада. Існує кілька рівнозначних варіантів визначення та реалізації, кожен з яких має дещо різні інтуїції щодо абстракції монади.

Функтор - це тип конструктора fтипу * -> *з екземпляром класу типу функтор.

{-# LANGUAGE KindSignatures #-}

class Functor (f :: * -> *) where
   map :: (a -> b) -> (f a -> f b)

На додаток до слідування протоколу стаціонарного типу, екземпляри класу типу функтора повинні підкорятися законам алгебраїчного функтора forall f g.

       map id  =  id           :: f t -> f t   Identity
map f . map g  =  map (f . g)  :: f a -> f c   Composition / short cut fusion

Функції обчислень мають тип

forall f t. Functor f => f t

Обчислення c rполягає в результатах r в контексті c .

Унарні монадні функції або стрілки Клейслі мають тип

forall m a b. Functor m => a -> m b

Стрілки Клейсі - це функції, які беруть один аргумент aі повертають монадичні обчислення m b.

Монади канонічно визначаються з точки зору трійки Клейслі forall m. Functor m =>

(m, return, (=<<))

реалізований як клас типу

class Functor m => Monad m where
   return :: t -> m t
   (=<<)  :: (a -> m b) -> m a -> m b

infixr 1 =<<

Ідентичність Клейла return є Клейл стрілок , яка сприяє значенням tв монадичну контексті m. Додаток Extension або Kleisli =<< застосовує стрілку Kleisli a -> m bдо результатів обчислення m a.

Склад Клейслі <=< визначається з точки зору розширення як

(<=<) :: Monad m => (b -> m c) -> (a -> m b) -> (a -> m c)
f <=< g = \ x -> f =<< g x

infixr 1 <=<

<=< складає дві стрілки Клейслі, застосовуючи стрілку ліворуч до результатів програми правої стрілки.

Екземпляри класу типу монада повинні підкорятися законам монади , найбільш елегантно викладеним з точки зору складу Клейслі:forall f g h.

   f <=< return  =  f                :: c -> m d   Right identity
   return <=< g  =  g                :: b -> m c   Left identity
(f <=< g) <=< h  =  f <=< (g <=< h)  :: a -> m d   Associativity

<=<є асоціативним і returnє його правою та лівою тотожністю.

Ідентичність

Тип особи

type Id t = t

- це функція ідентичності для типів

Id :: * -> *

Інтерпретується як функтор,

   return :: t -> Id t
=      id :: t ->    t

    (=<<) :: (a -> Id b) -> Id a -> Id b
=     ($) :: (a ->    b) ->    a ->    b

    (<=<) :: (b -> Id c) -> (a -> Id b) -> (a -> Id c)
=     (.) :: (b ->    c) -> (a ->    b) -> (a ->    c)

У канонічній Хаскеллі визначається ідентичність монади

newtype Id t = Id t

instance Functor Id where
   map :: (a -> b) -> Id a -> Id b
   map f (Id x) = Id (f x)

instance Monad Id where
   return :: t -> Id t
   return = Id

   (=<<) :: (a -> Id b) -> Id a -> Id b
   f =<< (Id x) = f x

Варіант

Тип опції

data Maybe t = Nothing | Just t

кодує обчислення, Maybe tякі не обов'язково дають результат t, обчислення, які можуть "провалитись". Визначений варіант монади

instance Functor Maybe where
   map :: (a -> b) -> (Maybe a -> Maybe b)
   map f (Just x) = Just (f x)
   map _ Nothing  = Nothing

instance Monad Maybe where
   return :: t -> Maybe t
   return = Just

   (=<<) :: (a -> Maybe b) -> Maybe a -> Maybe b
   f =<< (Just x) = f x
   _ =<< Nothing  = Nothing

a -> Maybe bзастосовується до результату лише в тому випадку, якщо Maybe aдає результат.

newtype Nat = Nat Int

Натуральні числа можна кодувати як цілі числа, що перевищують нуль.

toNat :: Int -> Maybe Nat
toNat i | i >= 0    = Just (Nat i)
        | otherwise = Nothing

Натуральні числа не закриваються на відніманні.

(-?) :: Nat -> Nat -> Maybe Nat
(Nat n) -? (Nat m) = toNat (n - m)

infixl 6 -?

Опція монади охоплює основну форму обробки винятків.

(-? 20) <=< toNat :: Int -> Maybe Nat

Список

Монада списку над типом списку

data [] t = [] | t : [t]

infixr 5 :

та його додаткова моноїдна операція "додавати"

(++) :: [t] -> [t] -> [t]
(x : xs) ++ ys = x : xs ++ ys
[]       ++ ys = ys

infixr 5 ++

кодує нелінійні обчислення, [t]даючи природну кількість 0, 1, ...результатів t.

instance Functor [] where
   map :: (a -> b) -> ([a] -> [b])
   map f (x : xs) = f x : map f xs
   map _ []       = []

instance Monad [] where
   return :: t -> [t]
   return = (: [])

   (=<<) :: (a -> [b]) -> [a] -> [b]
   f =<< (x : xs) = f x ++ (f =<< xs)
   _ =<< []       = []

Розширення =<<об'єднує ++всі списки, [b]отримані в результаті застосування f xстрілки Kleisli, a -> [b]до елементів [a]одного списку результатів [b].

Нехай власні подільники натурального числа nбути

divisors :: Integral t => t -> [t]
divisors n = filter (`divides` n) [2 .. n - 1]

divides :: Integral t => t -> t -> Bool
(`divides` n) = (== 0) . (n `rem`)

тоді

forall n.  let { f = f <=< divisors } in f n   =   []

При визначенні класу типу монада замість розширення =<<стандарт Haskell використовує його фліп, оператор зв'язування>>= .

class Applicative m => Monad m where
   (>>=) :: forall a b. m a -> (a -> m b) -> m b

   (>>) :: forall a b. m a -> m b -> m b
   m >> k = m >>= \ _ -> k
   {-# INLINE (>>) #-}

   return :: a -> m a
   return = pure

Для простоти в цьому поясненні використовується ієрархія класів типів

class              Functor f
class Functor m => Monad m

У Хаскелл поточна стандартна ієрархія

class                  Functor f
class Functor p     => Applicative p
class Applicative m => Monad m

тому що не тільки кожна монада є функціонером, але кожна програма - функтор, і кожна монада також є додатковою.

Використовуючи монаду списку, імперативний псевдокод

for a in (1, ..., 10)
   for b in (1, ..., 10)
      p <- a * b
      if even(p)
         yield p

приблизно перекладається на блок "do" ,

do a <- [1 .. 10]
   b <- [1 .. 10]
   let p = a * b
   guard (even p)
   return p

рівнозначне розуміння монади ,

[ p | a <- [1 .. 10], b <- [1 .. 10], let p = a * b, even p ]

і вираз

[1 .. 10] >>= (\ a ->
   [1 .. 10] >>= (\ b ->
      let p = a * b in
         guard (even p) >>       -- [ () | even p ] >>
            return p
      )
   )

Позначення та розуміння монади є синтаксичним цукром для вкладених виразів зв’язування. Оператор зв'язування використовується для прив'язки локальних імен до монадичних результатів.

let x = v in e    =   (\ x -> e)  $  v   =   v  &  (\ x -> e)
do { r <- m; c }  =   (\ r -> c) =<< m   =   m >>= (\ r -> c)

де

(&) :: a -> (a -> b) -> b
(&) = flip ($)

infixl 0 &

Визначена функція охорони

guard :: Additive m => Bool -> m ()
guard True  = return ()
guard False = fail

де тип одиниці або "порожній кортеж"

data () = ()

Аддитивні монади, що підтримують вибір та невдачу, можуть бути абстраговані за допомогою класу типу

class Monad m => Additive m where
   fail  :: m t
   (<|>) :: m t -> m t -> m t

infixl 3 <|>

instance Additive Maybe where
   fail = Nothing

   Nothing <|> m = m
   m       <|> _ = m

instance Additive [] where
   fail = []
   (<|>) = (++)

де failі <|>утворюють моноїдforall k l m.

     k <|> fail  =  k
     fail <|> l  =  l
(k <|> l) <|> m  =  k <|> (l <|> m)

і failє поглинаючим / знищуючим нульовим елементом добавок монади

_ =<< fail  =  fail

Якщо в

guard (even p) >> return p

even pвірно, тоді охорона виробляє [()]і, за визначенням >>, локальну постійну функцію

\ _ -> return p

застосовується до результату (). Якщо помилково, тоді охорона створює список монади fail( []), який не дає результату для застосування стрілки Клейслі >>, тому це pпропускається.

Держава

Ганебно монади використовуються для кодування обчислень.

Стан процесора є функцією

forall st t. st -> (t, st)

що переходить у стан stі дає результат t. Стан st може бути що завгодно. Нічого, прапор, кількість, масив, ручка, машина, світ.

Зазвичай називають тип процесорів стану

type State st t = st -> (t, st)

Монада державного процесора - це добрий * -> *функтор State st. Стрілки Клейслі монади державного процесора - це функції

forall st a b. a -> (State st) b

У канонічному Haskell визначається лінива версія монади стану процесора

newtype State st t = State { stateProc :: st -> (t, st) }

instance Functor (State st) where
   map :: (a -> b) -> ((State st) a -> (State st) b)
   map f (State p) = State $ \ s0 -> let (x, s1) = p s0
                                     in  (f x, s1)

instance Monad (State st) where
   return :: t -> (State st) t
   return x = State $ \ s -> (x, s)

   (=<<) :: (a -> (State st) b) -> (State st) a -> (State st) b
   f =<< (State p) = State $ \ s0 -> let (x, s1) = p s0
                                     in  stateProc (f x) s1

Процесор стану запускається шляхом подачі початкового стану:

run :: State st t -> st -> (t, st)
run = stateProc

eval :: State st t -> st -> t
eval = fst . run

exec :: State st t -> st -> st
exec = snd . run

Доступ держави забезпечується примітивами getта putметодами абстракції над державними монадами:

{-# LANGUAGE MultiParamTypeClasses, FunctionalDependencies #-}

class Monad m => Stateful m st m -> st where
   get :: m st
   put :: st -> m ()

m -> stдекларує функціональну залежність типу держави stвід монади m; наприклад, що State t, наприклад, визначатиме тип стану tоднозначно.

instance Stateful (State st) st where
   get :: State st st
   get = State $ \ s -> (s, s)

   put :: st -> State st ()
   put s = State $ \ _ -> ((), s)

з типом блоку, що використовується аналогічно до voidC.

modify :: Stateful m st => (st -> st) -> m ()
modify f = do
   s <- get
   put (f s)

gets :: Stateful m st => (st -> t) -> m t
gets f = do
   s <- get
   return (f s)

gets часто використовується з приладдями запису поля.

Еквівалент монади стану змінної різьби

let s0 = 34
    s1 = (+ 1) s0
    n = (* 12) s1
    s2 = (+ 7) s1
in  (show n, s2)

де s0 :: Int, не менш референтно прозорий, але нескінченно більш елегантний та практичний

(flip run) 34
   (do
      modify (+ 1)
      n <- gets (* 12)
      modify (+ 7)
      return (show n)
   )

modify (+ 1)- це обчислення типу State Int (), за винятком ефекту, еквівалентного return ().

(flip run) 34
   (modify (+ 1) >>
      gets (* 12) >>= (\ n ->
         modify (+ 7) >>
            return (show n)
      )
   )

Закон монади асоціативності можна записати в термінах >>= forall m f g.

(m >>= f) >>= g  =  m >>= (\ x -> f x >>= g)

або

do {                 do {                   do {
   r1 <- do {           x <- m;                r0 <- m;
      r0 <- m;   =      do {            =      r1 <- f r0;
      f r0                 r1 <- f x;          g r1
   };                      g r1             }
   g r1                 }
}                    }

Як і в програмі, орієнтованому на експресію (наприклад, Rust), остання заява блоку представляє його вихід. Оператор прив'язки іноді називають «програмованою крапкою з комою».

Примітиви структури управління ітерацією від структурованого імперативного програмування імітуються монадно

for :: Monad m => (a -> m b) -> [a] -> m ()
for f = foldr ((>>) . f) (return ())

while :: Monad m => m Bool -> m t -> m ()
while c m = do
   b <- c
   if b then m >> while c m
        else return ()

forever :: Monad m => m t
forever m = m >> forever m

Введення-виведення

data World

Монада процесорів світового стану вводу / виводу - це узгодження чистого Хаскелла і реального світу, функціональної денотативної та імперативної оперативної семантики. Близький аналог реальної суворої реалізації:

type IO t = World -> (t, World)

Взаємодія сприяє нечистим примітивам

getChar         :: IO Char
putChar         :: Char -> IO ()
readFile        :: FilePath -> IO String
writeFile       :: FilePath -> String -> IO ()
hSetBuffering   :: Handle -> BufferMode -> IO ()
hTell           :: Handle -> IO Integer
. . .              . . .

Домішка коду, що використовує IOпримітиви, постійно протоколізується системою типів. Тому що чистота є приголомшливою, те, що відбувається IO, залишається в ній IO.

unsafePerformIO :: IO t -> t

Або, принаймні, слід.

Підпис типу програми Haskell

main :: IO ()
main = putStrLn "Hello, World!"

розширюється до

World -> ((), World)

Функція, яка перетворює світ.

Епілог

Категорія, якою є об'єкти, є типи Хаскелла, а морфізми яких функціонують між типами Хаскелла, категорія - "швидка і вільна" Hask.

Функтор T- це відображення від категорії Cдо категорії D; для кожного об'єкта в Cоб'єкті вD

Tobj :  Obj(C) -> Obj(D)
   f :: *      -> *

і для кожного морфізму в Cморфізмі вD

Tmor :  HomC(X, Y) -> HomD(Tobj(X), Tobj(Y))
 map :: (a -> b)   -> (f a -> f b)

де X, Yє об’єкти в C. HomC(X, Y)є класом гомоморфізму всіх морфізмів X -> Yу Росії C. Функтор повинен зберігати морфізм ідентичність та композицію, "структуру" C, в D.

                    Tmor    Tobj

      T(id)  =  id        : T(X) -> T(X)   Identity
T(f) . T(g)  =  T(f . g)  : T(X) -> T(Z)   Composition

Категорія Клейла з категорії Cдаються Клейлі трійки

<T, eta, _*>

ендофунктора

T : C -> C

( f), морфізм ідентичності eta( return) та оператор розширення *( =<<).

Кожен марфізм Клейслі в Росії Hask

      f :  X -> T(Y)
      f :: a -> m b

оператором розширення

   (_)* :  Hom(X, T(Y)) -> Hom(T(X), T(Y))
  (=<<) :: (a -> m b)   -> (m a -> m b)

дається морфізм у Haskросійській категорії Клейслі

     f* :  T(X) -> T(Y)
(f =<<) :: m a  -> m b

Склад у категорії Клейслі .Tнаведений у розширенні

 f .T g  =  f* . g       :  X -> T(Z)
f <=< g  =  (f =<<) . g  :: a -> m c

і задовольняє аксіоми категорії

       eta .T g  =  g                :  Y -> T(Z)   Left identity
   return <=< g  =  g                :: b -> m c

       f .T eta  =  f                :  Z -> T(U)   Right identity
   f <=< return  =  f                :: c -> m d

  (f .T g) .T h  =  f .T (g .T h)    :  X -> T(U)   Associativity
(f <=< g) <=< h  =  f <=< (g <=< h)  :: a -> m d

який, застосовуючи перетворення еквівалентності

     eta .T g  =  g
     eta* . g  =  g               By definition of .T
     eta* . g  =  id . g          forall f.  id . f  =  f
         eta*  =  id              forall f g h.  f . h  =  g . h  ==>  f  =  g

(f .T g) .T h  =  f .T (g .T h)
(f* . g)* . h  =  f* . (g* . h)   By definition of .T
(f* . g)* . h  =  f* . g* . h     . is associative
    (f* . g)*  =  f* . g*         forall f g h.  f . h  =  g . h  ==>  f  =  g

з точки зору продовження канонічно подано

               eta*  =  id                 :  T(X) -> T(X)   Left identity
       (return =<<)  =  id                 :: m t -> m t

           f* . eta  =  f                  :  Z -> T(U)      Right identity
   (f =<<) . return  =  f                  :: c -> m d

          (f* . g)*  =  f* . g*            :  T(X) -> T(Z)   Associativity
(((f =<<) . g) =<<)  =  (f =<<) . (g =<<)  :: m a -> m c

Монади можна також визначити не з розширенням Клейсліана, а природним перетворенням muу програмі, що називається join. Монада визначається з точки зору muтрійки над категорією Cендофактора

     T :  C -> C
     f :: * -> *

і дві природні трансформації

   eta :  Id -> T
return :: t  -> f t

    mu :  T . T   -> T
  join :: f (f t) -> f t

задоволення еквівалентів

       mu . T(mu)  =  mu . mu               :  T . T . T -> T . T   Associativity
  join . map join  =  join . join           :: f (f (f t)) -> f t

      mu . T(eta)  =  mu . eta       =  id  :  T -> T               Identity
join . map return  =  join . return  =  id  :: f t -> f t

Потім визначається клас типу монади

class Functor m => Monad m where
   return :: t -> m t
   join   :: m (m t) -> m t

Канонічна muреалізація опції монада:

instance Monad Maybe where
   return = Just

   join (Just m) = m
   join Nothing  = Nothing

concatфункція

concat :: [[a]] -> [a]
concat (x : xs) = x ++ concat xs
concat []       = []

є joinмонадою списку.

instance Monad [] where
   return :: t -> [t]
   return = (: [])

   (=<<) :: (a -> [b]) -> ([a] -> [b])
   (f =<<) = concat . map f

Реалізації joinможуть бути переведені з форми розширення, використовуючи еквівалентність

     mu  =  id*           :  T . T -> T
   join  =  (id =<<)      :: m (m t) -> m t

Зворотний переклад від muформи розширення надається

     f*  =  mu . T(f)     :  T(X) -> T(Y)
(f =<<)  =  join . map f  :: m a -> m b

Але чому теорія настільки абстрактна повинна бути корисною для програмування?

Відповідь проста: як комп'ютерні працівники, ми цінуємо абстракцію ! Коли ми розробляємо інтерфейс до програмного компонента, ми хочемо, щоб він якомога менше розкривав про реалізацію. Ми хочемо мати можливість замінити реалізацію багатьма альтернативами, багатьма іншими "екземплярами" тієї ж "концепції". Коли ми розробляємо загальний інтерфейс для багатьох бібліотек програм, ще важливішим є те, що обраний нами інтерфейс має різноманітні реалізації. Це загальне поняття монади, яке ми цінуємо так високо, тому що теорія категорій настільки абстрактна, що її концепції настільки корисні для програмування.

Тож навряд чи дивується те, що узагальнення монад, які ми представляємо нижче, також має тісний зв’язок із теорією категорій. Але ми підкреслюємо, що наша мета дуже практична: це не «впровадження теорії категорій», а пошук більш загального способу структури бібліотек-комбінаторів. Це просто наше щастя, що математики вже зробили багато роботи за нас!

від генералізації монадів до стрілок Джона Х'юза


4

Світ потребує чергової публікації в блозі монади, але я думаю, що це корисно для виявлення існуючих монад у дикій природі.

Трикутник Серпінського

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

Монади - фрактали. Враховуючи монадичну структуру даних, її значення можуть бути складені для формування іншого значення структури даних. Ось чому це корисно для програмування, і саме тому воно виникає у багатьох ситуаціях.


3
Ви маєте на увазі "що світ не потребує ..."? Хороша аналогія, хоча!
гровербой

@ icc97 ти маєш рацію - сенс досить зрозумілий. Сарказм ненавмисний, вибачається перед автором.
гровербой

Що потрібно світові - це ще одна нитка коментарів, що підтверджує сарказм, але якщо я читаю уважно, я написав, але так, щоб це було зрозуміло.
Євген Йокота


4

Нехай нижче " {| a |m}" відображає частину монадичних даних. Тип даних, який рекламує a:

        (I got an a!)
          /        
    {| a |m}

Функція, fзнає, як створити монаду, якби вона мала a:

       (Hi f! What should I be?)
                      /
(You?. Oh, you'll be /
 that data there.)  /
 /                 /  (I got a b.)
|    --------------      |
|  /                     |
f a                      |
  |--later->       {| b |m}

Тут ми бачимо функцію, fнамагається оцінити монаду, але її докоряють.

(Hmm, how do I get that a?)
 o       (Get lost buddy.
o         Wrong type.)
o       /
f {| a |m}

Функція,, fзнаходить спосіб витягнути це aза допомогою >>=.

        (Muaahaha. How you 
         like me now!?)       
    (Better.)      \
        |     (Give me that a.)
(Fine, well ok.)    |
         \          |
   {| a |m}   >>=   f

Мало що fзнає, монада і >>=в змові.

            (Yah got an a for me?)       
(Yeah, but hey    | 
 listen. I got    |
 something to     |
 tell you first   |
 ...)   \        /
         |      /
   {| a |m}   >>=   f

Але про що вони насправді говорять? Ну, це залежить від монади. Розмова виключно в рефераті має обмежене використання; Ви повинні мати певний досвід роботи з окремими монадами, щоб досягти розуміння.

Наприклад, тип даних Можливо

 data Maybe a = Nothing | Just a

має екземпляр монади, який буде діяти наступним чином ...

Де, якщо справа Just a

            (Yah what is it?)       
(... hm? Oh,      |
forget about it.  |
Hey a, yr up.)    | 
            \     |
(Evaluation  \    |
time already? \   |
Hows my hair?) |  |
      |       /   |
      |  (It's    |
      |  fine.)  /
      |   /     /    
   {| a |m}   >>=   f

Але для випадку о Nothing

        (Yah what is it?)       
(... There      |
is no a. )      |
  |        (No a?)
(No a.)         |
  |        (Ok, I'll deal
  |         with this.)
   \            |
    \      (Hey f, get lost.) 
     \          |   ( Where's my a? 
      \         |     I evaluate a)
       \    (Not any more  |
        \    you don't.    |
         |   We're returning
         |   Nothing.)   /
         |      |       /
         |      |      /
         |      |     /
   {| a |m}   >>=   f      (I got a b.)
                    |  (This is   \
                    |   such a     \
                    |   sham.) o o  \
                    |               o|
                    |--later-> {| b |m}

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

Різні монади поводяться по-різному. Списки - це інші типи даних із монадичними екземплярами. Вони поводяться так:

(Ok, here's your a. Well, its
 a bunch of them, actually.)
  |
  |    (Thanks, no problem. Ok
  |     f, here you go, an a.)
  |       |
  |       |        (Thank's. See
  |       |         you later.)
  |  (Whoa. Hold up f,      |
  |   I got another         |
  |   a for you.)           |
  |       |      (What? No, sorry.
  |       |       Can't do it. I 
  |       |       have my hands full
  |       |       with all these "b" 
  |       |       I just made.) 
  |  (I'll hold those,      |
  |   you take this, and   /
  |   come back for more  /
  |   when you're done   / 
  |   and we'll do it   / 
  |   again.)          /
   \      |  ( Uhhh. All right.)
    \     |       /    
     \    \      /
{| a |m}   >>=  f  

У цьому випадку функція знала, як зробити список із свого введення, але не знала, що робити з додатковими введеннями та додатковими списками. Пов’язування >>=, що сприяло fпоєднанню декількох результатів. Я включаю цей приклад, щоб показати, що, хоча >>=відповідає за видобуток a, він також має доступ до можливого пов'язаного виводу f. Дійсно, він ніколи не витягує жодного, aякщо не знає, що кінцевий результат має той же тип контексту.

Є й інші монади, які використовуються для представлення різних контекстів. Ось деякі характеристики ще кількох. У IOмонади насправді немає a, але він знає хлопця і отримає це aза вас. У State stмонади є таємна скринька, stяку вона передасть fпід стіл, навіть незважаючи на те, що fщойно прийшов просити a. Reader rМонада подібна State st, хоча вона дозволяє тільки fдивитися на r.

Сенс у всьому цьому полягає в тому, що будь-який тип даних, який оголосив себе монадою, декларує якийсь контекст навколо вилучення значення з монади. Великий прибуток від усього цього? Ну, це досить просто, щоб підрахувати розрахунок з якимось контекстом. Однак це може стати безладним, коли з'єднуються декілька обчислень, завантажених контекстом. Операції монади дбають про вирішення взаємодій контексту, щоб програмісту не довелося.

Зауважте, що використання >>=полегшує безлад, відбираючи частину автономії f. Тобто, у наведеному вище прикладі, Nothingнаприклад, fбільше не вирішується, що робити у випадку Nothing; він закодований >>=. Це компроміс. Якби це було необхідно для , fщоб вирішити , що робити в разі Nothing, то fповинно бути функцією від Maybe aдо Maybe b. У цьому випадку Maybeбути монадою не має значення.

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


3

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

Прикладом може бути введення / виведення файлу.

Ви зможете використовувати монаду для вводу / виводу файлів, щоб виділити зміну природи стану лише коду, який використовував Monad. Код всередині Монади може ефективно ігнорувати мінливий стан світу поза Монадою - це значно спрощує міркування про загальний ефект вашої програми.


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