Монада простою англійською? (Для програміста OOP без фонового режиму)


743

З точки зору, який програміст ООП зрозумів (без будь-якого функціонального фону програмування), що таке монада?

Яку проблему вона вирішує та які найпоширеніші місця, якими вона користується?

Редагувати:

Щоб уточнити, яке саме розуміння я шукав, скажімо, ви перетворювали FP-додаток, у якого були монади, у додаток OOP. Що б ви зробили, щоб перекласти обов'язки монад на додаток OOP?


10
Ця публікація в блозі дуже хороша: blog.sigfpe.com/2006/08/you-could-have-invented-monads-and.html
Паскаль Куок


10
@Pavel: Відповідь, яку ми отримали нижче від Еріка, набагато краща, ніж відповіді в інших запропонованих питаннях Q для людей з фоном OO (на відміну від фонового режиму FP).
Стипендіати доналу

5
@Donal: Якщо це простак (про яку я не маю ніякого думки), хороший відповідь повинен бути доданий до оригіналу. Тобто: хороша відповідь не виключає закриття в якості дубліката. Якщо це досить близький дублікат, це може зробити модератор у вигляді злиття.
dmckee --- кошеня колишнього модератора

3
Дивіться також: stackoverflow.com/questions/674855/…
sth

Відповіді:


732

ОНОВЛЕННЯ: Це питання було предметом надзвичайно довгої серії блогу, яку ви можете прочитати в Monads - дякую за чудове запитання!

З точки зору, який програміст ООП зрозумів (без будь-якого функціонального фону програмування), що таке монада?

Монада - це "підсилювач" типів, який підкоряється певним правилам і який передбачає певні операції .

По-перше, що таке "підсилювач типів"? Під цим я маю на увазі деяку систему, яка дозволяє взяти тип і перетворити його на більш спеціальний тип. Наприклад, в C # розглянемо Nullable<T>. Це підсилювач типів. Це дозволяє вам вибрати тип, скажімо int, і додати нову можливість до цього типу, а саме: тепер він може бути нульовим, коли раніше не міг.

В якості другого прикладу розглянемо IEnumerable<T>. Це підсилювач типів. Це дозволяє вам вибрати тип, скажімо, stringта додати нову можливість до цього типу, а саме: тепер ви можете складати послідовність рядків з будь-якої кількості одиничних рядків.

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

int M(int x) { return x + N(x * 2); }

то відповідна функція Nullable<int>може змусити всіх операторів і викликів там працювати разом "так само", як це робили раніше.

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

Що таке "операції"?

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

  2. Існує операція "прив'язування", яка приймає монадичне значення і функцію, яка може перетворити значення, і повертає нове монадичне значення. Прив’язка - це ключова операція, яка визначає семантику монади. Це дозволяє нам перетворити операції на неампліфікований тип в операції посиленого типу, що підкоряються згаданим раніше правилам функціонального складу.

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

Знову візьмемо Nullable<T>як приклад. Ви можете перетворити intа Nullable<int>в конструктор. Компілятор C # піклується про найнезмінніший "підйом" для вас, але якщо цього не сталося, підйомне перетворення просте: операція, скажімо,

int M(int x) { whatever }

перетворюється на

Nullable<int> M(Nullable<int> x) 
{ 
    if (x == null) 
        return null; 
    else 
        return new Nullable<int>(whatever);
}

І перетворення Nullable<int>спини в анкету intробиться з Valueвластивістю.

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

Припустимо , у вас є функція від intдо int, як наш оригінал M. Ви можете легко зробити це у функції, яка приймає intта повертає a, Nullable<int>тому що ви можете просто запустити результат через нульовий конструктор. Тепер припустимо, що у вас є такий метод вищого порядку:

static Nullable<T> Bind<T>(Nullable<T> amplified, Func<T, Nullable<T>> func)
{
    if (amplified == null) 
        return null;
    else
        return func(amplified.Value);
}

Бачите, що ви можете зробити з цим? Будь-який метод, який приймає intта повертає int, або приймає intта повертає, Nullable<int>тепер може застосувати до нього нульову семантику .

Крім того: припустимо, у вас є два методи

Nullable<int> X(int q) { ... }
Nullable<int> Y(int r) { ... }

і ви хочете скласти їх:

Nullable<int> Z(int s) { return X(Y(s)); }

Тобто, Zце склад Xі Y. Але ви не можете цього зробити, тому що Xприймає intта Yповертає Nullable<int>. Але оскільки у вас є операція "прив'язати", ви можете зробити цю роботу:

Nullable<int> Z(int s) { return Bind(Y(s), X); }

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

У C # "Bind" називається "SelectMany". Погляньте, як це працює на монаді послідовності. Нам потрібно мати дві речі: перетворити значення в послідовність і зв’язати операції на послідовності. Як бонус ми також маємо "перетворити послідовність назад у значення". Ці операції:

static IEnumerable<T> MakeSequence<T>(T item)
{
    yield return item;
}
// Extract a value
static T First<T>(IEnumerable<T> sequence)
{
    // let's just take the first one
    foreach(T item in sequence) return item; 
    throw new Exception("No first item");
}
// "Bind" is called "SelectMany"
static IEnumerable<T> SelectMany<T>(IEnumerable<T> seq, Func<T, IEnumerable<T>> func)
{
    foreach(T item in seq)
        foreach(T result in func(item))
            yield return result;            
}

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

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

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

static IEnumerable<U> SelectMany<T,U>(IEnumerable<T> seq, Func<T, IEnumerable<U>> func)
{
    foreach(T item in seq)
        foreach(U result in func(item))
            yield return result;            
}

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

Яку проблему вона вирішує та які найпоширеніші місця, якими вона користується?

Це скоріше, як запитати "які проблеми вирішує одинарний візерунок?", Але я дам йому змогу.

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

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

C # використовує монади у своїй конструкції. Як уже було сказано, нульовий зразок сильно схожий на "можливо монаду". LINQ повністю побудований з монад; SelectManyметод , що робить семантичну роботу складу операцій. (Ерік Мейєр любить вказувати, що кожну функцію LINQ можна реально реалізувати SelectMany; все інше - це лише зручність.)

Щоб уточнити, яке саме розуміння я шукав, скажімо, ви перетворювали FP-додаток, у якого були монади, у додаток OOP. Що б ви зробили, щоб перекласти обов'язки монадів у додаток OOP?

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

Гарне місце для початку - це те, як ми реалізували LINQ в C #. Вивчити SelectManyметод; це ключ до розуміння того, як працює монада послідовностей у C #. Це дуже простий метод, але дуже потужний!


Пропоноване подальше читання:

  1. Для більш поглибленого та теоретично обґрунтованого пояснення монад у C #, я настійно рекомендую статтю мого колеги ( Еріка Ліпперта ) Уеса Дайєра на цю тему. Ця стаття - це те, що мені пояснили монади, коли вони нарешті "натиснули" на мене.
  2. Хороша ілюстрація того, чому ви можете хотіти монаду навколо (використовує Haskell у своїх прикладах) .
  3. Сортування, "переклад" попередньої статті в JavaScript.


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

5
Відмінне пояснення, як зазвичай Ерік. Для більш теоретичного (але все-таки дуже цікавого) обговорення я знайшов повідомлення про блог Барта Де Смета на MinLINQ корисним у відношенні деяких функціональних програм програмування назад до C #. community.bartdesmet.net/blogs/bart/archive/2010/01/01/…
Ron Warholic

41
Мені більше сенсу говорити, що вона збільшує типи, а не посилює їх.
Гейб

61
@slomojo: і я змінив це до того, що написав і мав намір написати. Якщо ви з Гейбом хочете написати власну відповідь, ви йдете прямо вперед.
Ерік Ліпперт

24
@Eric, до вас, звичайно, але підсилювач означає, що наявні властивості посилюються, що вводить в оману.
ocodo

341

Навіщо нам потрібні монади?

  1. Ми хочемо програмувати лише за допомогою функцій . ("функціональне програмування" зрештою -FP).
  2. Тоді у нас є перша велика проблема. Це програма:

    f(x) = 2 * x

    g(x,y) = x / y

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

    Рішення: складати функції . Якщо ви хочете спочатку, gа потім fпросто пишіть f(g(x,y)). Гаразд, але ...

  3. Більше проблем: деякі функції можуть вийти з ладу (тобто g(2,0)розділити на 0). У «ПП» у нас немає «винятків» . Як ми її вирішуємо?

    Рішення: Дозвольмо функціям повертати два види речей : замість того, щоб мати g : Real,Real -> Real(функція з двох реальних в реальну), давайте дозволимо g : Real,Real -> Real | Nothing(функція з двох реальних в (справжніх або нічого)).

  4. Але функції повинні (бути простішими) повертати лише одне .

    Рішення: давайте створимо новий тип даних, що підлягає поверненню, " тип боксу ", який закриває, можливо, справжній або просто нічого. Отже, ми можемо мати g : Real,Real -> Maybe Real. Гаразд, але ...

  5. Що зараз відбувається з f(g(x,y))? fне готовий до споживання Maybe Real. І ми не хочемо змінювати будь-яку функцію, з якою ми могли б з'єднатися, gщоб споживати a Maybe Real.

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

    У нашому випадку: g >>= f(підключити / скласти gдо f). Ми хочемо >>=отримати gвихід, перевірити його і, якщо він Nothingпросто не дзвонить fі не повертається Nothing; або, навпаки, витягати коробку Realі годувати fнею. (Цей алгоритм є лише реалізацією >>=для Maybeтипу).

  6. Існує багато інших проблем, які можна вирішити за допомогою цього самого шаблону: 1. Використовуйте "поле" для кодування / зберігання різних значень / значень, і виконайте такі функції, gщо повертають ці "коробкові значення". 2. g >>= fДопоможіть композиторам / лінкерам, які допоможуть підключити gвихід до fвходу, тому нам зовсім не доведеться змінювати f.

  7. Чудовими проблемами, які можна вирішити за допомогою цієї методики, є:

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

    • Нам не подобаються "нечисті функції": функції, які дають різний вихід за один і той же вхід. Тому позначимо ці функції, зробивши їх поверненням позначеного / коробкового значення: IOмонада.

Тотальне щастя !!!!


2
@DmitriZaitsev Винятки можуть мати місце лише в "нечистому коді" (монада IO), наскільки я знаю.
cibercitizen1

3
@DmitriZaitsev Роль Нічого не може грати будь-який інший тип (різний від очікуваного Реального). У цьому справа не в тому. У прикладі справа полягає в тому, як адаптувати функції в ланцюжку, коли попередній може повернути несподіваний тип значення наступному, не прив'язуючи останній (приймаючи лише Real як вхід).
cibercitizen1

3
Інша точка плутанини в тому , що слово «монада» з'являється тільки двічі в своїй відповіді, і тільки в поєднанні з іншими термінами - Stateі IO, ні з ким з них, а також точного значення «монади» давав
Дмитро Зайцев

31
Для мене як особистості, що походить з ОП, ця відповідь справді добре пояснює мотивацію монади, а також, що насправді є монадою (набагато більше, ніж прийнята відповідь). Отже, я вважаю це дуже корисним. Велике спасибі @ cibercitizen1 та +1
akhilless

3
Я читав про вимкнення функціонального програмування близько року. Ця відповідь, і особливо перші два пункти, нарешті дала мені зрозуміти, що насправді означає імперативне програмування та чому функціональне програмування відрізняється. Дякую!
jrahhali

82

Я б сказав, що найближча аналогія ОО до монад - це " командна схема ".

У командному шаблоні ви загортаєте звичайний вираз або вираз у командний об'єкт. Об'єкт команди відкриває метод Execute, який виконує завершений оператор. Таким чином, оператор перетворюється на об'єкти першого класу, які можуть передаватися і виконуватись за бажанням. Команди можуть бути складені, щоб ви могли створити програмний об’єкт за допомогою прив'язування та вставки командних об’єктів.

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

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

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

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


15
Я вважаю, що це перше пояснення монади, яке я бачив, що не спирався на концепції функціонального програмування і не вкладав його в реальні терміни OOP. Дійсно хороша відповідь.
Девід К. Гесс

це дуже близько 2, які монади насправді є у FP / Haskell, за винятком того, що самі об'єкти команди "знають", до якої "логіки виклику" вони належать (і лише сумісні можуть бути пов'язані разом); Провідник лише постачає перше значення. Це не так, як команда "Друк" може бути виконана "недетермінованою логікою виконання". Ні, це має бути "логіка вводу / виводу" (тобто монада IO). Але крім цього, це дуже близько. Можна навіть просто сказати, що Monads - це лише Програми (побудовані з кодових виписок, які потрібно виконати пізніше). У перші дні про "зв'язування" говорили як про "програмовану крапку з комою" .
Буде Несс

1
@ DavidK.Hess Я справді неймовірно скептично ставляться до відповідей, які використовують FP для пояснення основних понять FP, а особливо відповідей, які використовують мову FP, як Scala. Молодці, ЖакБ!
Моніку

62

З точки зору, який програміст ООП зрозумів (без будь-якого функціонального фону програмування), що таке монада?

Яку проблему вона вирішує та які найпоширеніші місця, якими вона користується? Чи це найпоширеніші місця, якими вона використовується?

З точки зору програмування OO, монада є інтерфейс (або скоріше домішка), параметризрвані типу, з двома методами, returnі bindякі описують:

  • Як ввести значення, щоб отримати монадичне значення цього типу введеного значення;
  • Як використовувати функцію, яка робить монадичне значення з немонадного, на монадичне значення.

Проблема, яку вона вирішує, - це той самий тип проблеми, якого ви очікували від будь-якого інтерфейсу, а саме: "У мене є маса різних класів, які роблять різні речі, але, здається, роблять ці різні речі таким чином, що має основну схожість. Як Чи можу я описати подібність між ними, навіть якщо самі класи насправді не є підтипами нічого ближчого, ніж клас "Об'єкт"? "

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


1
returnнасправді не буде методом монади, тому що він не бере аргумент монади. (тобто: цього немає / я)
Лоранс Гонсалвес

@LaurenceGonsalves: Оскільки я зараз розглядаю це для своєї дипломної роботи, я думаю, що в основному обмежує це відсутність статичних методів в інтерфейсах на C # / Java. Ви можете пройти далекий шлях у напрямку реалізації всієї історії монади, принаймні статично пов'язаної замість того, щоб грунтуватися на класах типу. Цікаво, що це навіть спрацювало, незважаючи на відсутність вищого роду.
Себастьян Граф

42

У вас відбулася нещодавня презентація " Монадологія - професійна допомога при тривозі типів " Крістофера Ліги (12 липня 2010 р.), Яка досить цікава на теми продовження та монади.
Відео з цією (слайд-презентацією) презентацією насправді доступне у vimeo .
Частина Monad розпочинається приблизно через 37 хвилин із цього годинного відео та починається зі слайда 42 із 58 слайд-презентацій.

Він представлений як "провідна модель дизайну для функціонального програмування", але мова, що використовується в прикладах, - Scala, яка є як OOP, так і функціональною.
Ви можете прочитати більше про Monad in Scala в публікації в блозі " Monads - ще один спосіб абстрактних обчислень у Scala ", з Debasish Ghosh (27 березня 2008 р.).

Конструктор типу M є монадою, якщо він підтримує ці операції:

# the return function
def unit[A] (x: A): M[A]

# called "bind" in Haskell 
def flatMap[A,B] (m: M[A]) (f: A => M[B]): M[B]

# Other two can be written in term of the first two:

def map[A,B] (m: M[A]) (f: A => B): M[B] =
  flatMap(m){ x => unit(f(x)) }

def andThen[A,B] (ma: M[A]) (mb: M[B]): M[B] =
  flatMap(ma){ x => mb }

Так, наприклад (у Scala):

  • Option є монадою
    def unit [A] (x: A): варіант [A] = деякий (x)

    def flatMap [A, B] (m: Варіант [A]) (f: A => Варіант [B]): Варіант [B] =
      м збіг {
       випадок None => None
       випадок Some (x) => f (x)
      }
  • List є Монада
    def unit [A] (x: A): список [A] = список (x)

    def flatMap [A, B] (m: Список [A]) (f: A => Список [B]): список [B] =
      м збіг {
        випадок Nil => Nil
        випадок x :: xs => f (x) ::: flatMap (xs) (f)
      }

Monad - велика справа у Scala через зручний синтаксис, побудований для того, щоб скористатися структурами Monad:

forрозуміння в Scala :

for {
  i <- 1 to 4
  j <- 1 to i
  k <- 1 to j
} yield i*j*k

компілятор перекладається на:

(1 to 4).flatMap { i =>
  (1 to i).flatMap { j =>
    (1 to j).map { k =>
      i*j*k }}}

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

У наведеному вище фрагменті flatMap приймає за вхід закриття (SomeType) => List[AnotherType]та повертає a List[AnotherType]. Важливим моментом, який слід зазначити, є те, що всі плоскі карти приймають той самий тип закриття, що і вхід, і повертають той же тип, що і вихідний.

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


Якщо ви зробите дві операції (які можуть бути невдалими) і передаєте результат третій, наприклад:

lookupVenue: String => Option[Venue]
getLoggedInUser: SessionID => Option[User]
reserveTable: (Venue, User) => Option[ConfNo]

але не скориставшись монадою, ви отримаєте спірний OOP-код на зразок:

val user = getLoggedInUser(session)
val confirm =
  if(!user.isDefined) None
  else lookupVenue(name) match {
    case None => None
    case Some(venue) =>
      val confno = reserveTable(venue, user.get)
      if(confno.isDefined)
        mailTo(confno.get, user.get)
      confno
  }

тоді як з монадою ви можете працювати з фактичними типами ( Venue, User), як і всі операції, і зберігати параметри перевірки параметрів прихованими, все з-за плоских карт синтаксису:

val confirm = for {
  venue <- lookupVenue(name)
  user <- getLoggedInUser(session)
  confno <- reserveTable(venue, user)
} yield {
  mailTo(confno, user)
  confno
}

Частина виходу буде виконуватися лише за наявності всіх трьох функцій Some[X]; будь-який Noneбуде безпосередньо повернутий confirm.


Тому:

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

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

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


До речі, Monad - це не лише модель обчислень, яка використовується у FP:

Теорія категорій пропонує багато моделей обчислень. Серед них

  • модель обчислень стрілки
  • модель обчислень Монади
  • Прикладна модель обчислень

2
Я люблю це пояснення! Наведений вами приклад прекрасно демонструє концепцію, а також додає те, чого IMHO не бракувало в тирі Еріка про те, що SelectMany () є монадою. Thx для цього!
1111

1
ІМХО це найелегантніша відповідь
Полімераза

і перед усім іншим, Functor.
Буде Несс

34

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

Ось і стисле, і чітке визначення, дещо перероблене:

Монада (в інформатиці) формально карта , яка:

  • посилає кожен тип Xдеякої заданої мови програмування до нового типу T(X)(називається "тип T-вичислень зі значеннями в X");

  • оснащений правилом складання двох функцій форми f:X->T(Y)та g:Y->T(Z)функції g∘f:X->T(Z);

  • таким чином, який є асоціативним у явному сенсі та неінітальним щодо заданої одиничної функції, яка називається pure_X:X->T(X), вважати таким, що приймає значення до чистого обчислення, яке просто повертає це значення.

Отже, простими словами, монада - це правило переходу від будь-якого типу Xдо іншого типуT(X) , а правило - перехід від двох функцій f:X->T(Y)і g:Y->T(Z)(що ви хотіли б скласти, але не можете) до нової функціїh:X->T(Z) . Що, однак, не є композицією в суворому математичному сенсі. Ми в основному "згинаємо" склад функції або переосмислюємо, як складаються функції.

Крім того, нам потрібно правило складання монади, щоб задовольнити "очевидні" математичні аксіоми:

  • Асоціативність : Композиція fз, gа потім із h(ззовні) повинна бути такою ж, як композиція gз, hа потім із f(зсередини).
  • Унітальна властивість : Композиція fз функцією тотожності з будь-якої сторони має принести результат f.

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

  • Спочатку нам потрібна асоціативність, щоб мати можливість складати кілька функцій підряд, наприклад f(g(h(k(x))), і не турбуватися про те, щоб вказати пари функцій, що складають функції. Оскільки правило монади лише прописує, як скласти пару функцій , без цієї аксіоми нам слід було б знати, яка пара складається першою тощо. (Зверніть увагу , що відрізняється від властивості коммутативности , що в fскладі з gбули такими ж , як у gскладі з f, що не потрібно).
  • По-друге, нам потрібна унітальна властивість, яка просто говорить про те, що особистість складається тривіально так, як ми їх очікуємо. Таким чином, ми можемо безпечно виконувати функції рефактора кожного разу, коли ці особи можна отримати.

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

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

Це, по суті, в двох словах.


Будучи професійним математиком, я вважаю за краще уникати називати h"склад" fта g. Тому що математично це не так. Називаючи це "композицією", неправильно припускається, що hце справжній математичний склад, який він не є. Це навіть не однозначно визначається fі g. Натомість це результат нового "правила складання" монади нашої монади. Що може абсолютно відрізнятися від фактичного математичного складу, навіть якщо останній існує!


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

Кидання винятків як приклади Монади

Припустимо, ми хочемо скласти дві функції:

f: x -> 1 / x
g: y -> 2 * y

Але f(0)не визначено, тому eвикидається виняток . Тоді як можна визначити композиційне значення g(f(0))? Знову киньте виняток, звичайно! Може, те саме e. Можливо, новий оновлений виняток e1.

Що саме тут відбувається? По-перше, нам потрібні нові значення (и) виключення (різні або однакові). Ви можете назвати їх nothingабо nullабо будь-який інший, але суть залишається та ж - вони повинні бути нові значення, наприклад , це не повинно бути numberв нашому прикладі тут. Я вважаю за краще не називати їх, nullщоб уникнути плутанини з тим, як nullможна реалізувати будь-яку конкретну мову. Так само я вважаю за краще уникати, nothingтому що це часто асоціюється з тим null, що, в принципі, є тим, що nullслід робити, однак цей принцип часто збивається з будь-яких практичних причин.

Що саме є винятком?

Це дрібниця для будь-якого досвідченого програміста, але я хотів би залишити кілька слів, щоб усунути будь-якого хробака плутанини:

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

Це може варіюватися від викидання будь-яких деталей і повернення єдиного глобального значення (наприклад, NaNабо null) або створення довгого списку журналів або того, що саме сталося, надсилання його в базу даних та реплікації по всьому розподіленому шару зберігання даних;)

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

Чи допускаються винятки в чистих функціях?

Коротша відповідь : Так, але лише тоді, коли вони не призводять до побічних ефектів.

Більш довга відповідь. Щоб бути чистим, результат вашої функції повинен бути однозначно визначений її входом. Таким чином, ми вносимо зміни до своєї функції f, надсилаючи 0нове абстрактне значення, eяке ми називаємо винятком. Ми переконуємося, що значення eне містить зовнішньої інформації, яка не визначається однозначно нашим вкладом, що є x. Ось ось приклад виключення без побічних ефектів:

e = {
  type: error, 
  message: 'I got error trying to divide 1 by 0'
}

І ось один із побічними ефектами:

e = {
  type: error, 
  message: 'Our committee to decide what is 1/0 is currently away'
}

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

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

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

Тип розширення

Оскільки ми хочемо змінити повідомлення всередині нашого винятку, ми дійсно оголошуємо новий тип Eдля всього об’єкта винятку, і тоді це maybe numberробиться, окрім його заплутаного імені, яке має бути або типу, numberабо нового типу винятку E, так що це дійсно союз number | Eз numberі E. Зокрема, це залежить від того, як ми хочемо побудувати E, що ні запропоновано, ні відображено в назві maybe number.

Що таке функціональний склад?

Це математичні функції операції , які беруть f: X -> Yі g: Y -> Zта побудова їх складу як функція , h: X -> Zяка задовольнить h(x) = g(f(x)). Проблема з цим визначенням виникає, коли результат f(x)не дозволений як аргумент g.

У математиці ці функції неможливо скласти без додаткової роботи. Суворо математичне рішення для нашого вище прикладу fі gполягає в тому, щоб вилучити 0з набору визначення f. З цим новим набором визначення (новий більш обмежувальний тип x) fстає компонувати з g.

Однак в програмуванні не дуже практично обмежувати набір fподібного визначення . Натомість можна використовувати винятки.

Або як інший підхід, штучні цінності створюються , як NaN, undefined, null, і Infinityт.д. Таким чином , ви оцінюєте 1/0до Infinityі 1/-0до -Infinity. А потім примушуйте нове значення повертатися у свій вираз, а не викидати виняток. Приводячи до результатів, ви можете або не можете вважати передбачуваними:

1/0                // => Infinity
parseInt(Infinity) // => NaN
NaN < 0            // => false
false + 1          // => 1

І ми повертаємося до звичайних номерів, готових рухатися далі;)

JavaScript дозволяє нам будь-якою ціною виконувати числові вирази, не викидаючи помилок, як у наведеному вище прикладі. Це означає, що це також дозволяє складати функції. Що саме стосується монади - це правило складати функції, що відповідають аксіомам, визначеним на початку цієї відповіді.

Але чи є правилом функції складання, що випливає з реалізації JavaScript для роботи з числовими помилками, монадою?

Щоб відповісти на це запитання, все, що вам потрібно, це перевірити аксіоми (залишено як вправу, оскільки тут не є частиною запитання;).

Чи можна викид кидання використовувати для побудови монади?

Дійсно, більш корисною монадою було б замість цього правило, яке передбачає, що якщо fвикинути виняток для деяких x, то і його склад з будь-яким g. Плюс зробити виняток у Eвсьому світі унікальним лише одним можливим значенням ( термінальний об'єкт у теорії категорій). Тепер дві аксіоми миттєво перевіряються, і ми отримуємо дуже корисну монаду. І результат - це те, що добре відоме як монада, можливо .


3
Хороший внесок. +1 Але, можливо, ви хочете видалити, "ви знайшли більшість пояснень занадто довго ...", оскільки ваше найдовше взагалі. Інші будуть судити, чи це "звичайна англійська мова", як вимагає питання: "звичайна англійська == простими словами, по-простому".
cibercitizen1

@ cibercitizen1 Дякую! Це насправді коротко, якщо ви не рахуєте приклад. Головний момент - вам не потрібно читати приклад, щоб зрозуміти визначення . На жаль, багато пояснень змушують мене спочатку прочитати приклади , що часто є непотрібним, але, звичайно, може вимагати додаткової роботи для письменника. Занадто багато покладаючись на конкретні приклади, існує небезпека, що неважливі деталі затьмарюють малюнок і ускладнюють його розуміння. Сказавши це, у вас є дійсні точки, дивіться оновлення.
Дмитро Зайцев

2
занадто довгий і заплутаний
бачивімуруган

1
@seenimurugan Пропозиції щодо вдосконалення вітаються;)
Дмитро Зайцев

26

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

  • return x створює значення типу монади, яке інкапсулює x
  • m >>= f(читайте його як "оператор прив'язки") застосовує функцію fдо значення в монадіm

Ось що таке монада. Є ще кілька технічних можливостей , але в основному ці дві операції визначають монаду. Справжнє питання: «Що робить монада ?», І це залежить від монади - списки - це монади, Мейби - монади, операції МО - монади. Все, що це означає, коли ми говоримо, що ці речі є монадами, це те, що вони мають інтерфейс монади returnта >>=.


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

14

З Вікіпедії :

У функціональному програмуванні монада - це різновид абстрактного типу даних, який використовується для представлення обчислень (замість даних у доменній моделі). Монади дозволяють програмісту об'єднати дії разом для побудови трубопроводу, в якому кожна дія прикрашена додатковими правилами обробки, наданими монадою. Програми, написані у функціональному стилі, можуть використовувати монади для структуризації процедур, що включають в себе послідовні операції, 1 [2] або для визначення довільних потоків управління (наприклад, обробка одночасності, продовження чи винятки).

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

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

Я вважаю, це це дуже добре пояснює.


12

Я спробую зробити найкоротше визначення, яким я можу керувати, використовуючи терміни OOP:

Родовий клас CMonadic<T>- це монада, якщо вона визначає принаймні такі методи:

class CMonadic<T> { 
    static CMonadic<T> create(T t);  // a.k.a., "return" in Haskell
    public CMonadic<U> flatMap<U>(Func<T, CMonadic<U>> f); // a.k.a. "bind" in Haskell
}

і якщо наступні закони застосовуються для всіх типів T та їх можливих значень t

ліва особа:

CMonadic<T>.create(t).flatMap(f) == f(t)

правильна ідентичність

instance.flatMap(CMonadic<T>.create) == instance

асоціативність:

instance.flatMap(f).flatMap(g) == instance.flatMap(t => f(t).flatMap(g))

Приклади :

У монаді списку може бути:

List<int>.create(1) --> [1]

І flatMap у списку [1,2,3] може працювати так:

intList.flatMap(x => List<int>.makeFromTwoItems(x, x*10)) --> [1,10,2,20,3,30]

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

Коментар :

Монади не такі складні. flatMapФункція дуже багато , як більш часто зустрічається map. Він отримує аргумент функції (також відомий як делегат), який він може викликати (негайно чи пізніше, нуль або більше разів) зі значенням, що надходить із загального класу. Він очікує, що передана функція також зафіксує свою повернуту величину в тому ж родовому класі. Щоб допомогти у цьому, він пропонує createконструктор, який може створити екземпляр цього загального класу зі значення. Результат повернення flatMap також є загальним класом одного типу, часто пакуючи ті самі значення, які містилися в результатах повернення однієї або більше застосувань flatMap до раніше містяться значень. Це дозволяє ланцюжком FlatMap скільки завгодно:

intList.flatMap(x => List<int>.makeFromTwo(x, x*10))
       .flatMap(x => x % 3 == 0 
                   ? List<string>.create("x = " + x.toString()) 
                   : List<string>.empty())

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

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

Примітки : Функціональні мови дозволяють записувати функції, які можуть працювати на будь-якому типі монадичного загального класу. Для цього потрібно було б написати загальний інтерфейс для монад. Я не знаю, чи можливо написати такий інтерфейс на C #, але наскільки я знаю, це не так:

interface IMonad<T> { 
    static IMonad<T> create(T t); // not allowed
    public IMonad<U> flatMap<U>(Func<T, IMonad<U>> f); // not specific enough,
    // because the function must return the same kind of monad, not just any monad
}

7

Чи монада має "природне" тлумачення в ОО, залежить від монади. У такій мові, як Java, ви можете перевести монаду, можливо, мовою перевірки нульових покажчиків, так що обчислення, які не вдається (тобто не дають нічого в Haskell), видають нульові покажчики як результати. Ви можете перевести монаду стану на створену мову, створивши змінну змінну та методи змінити її стан.

Монада - це моноїд у категорії ендофаніків.

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

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

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

Для цього можна використовувати монадні трансформатори, і існує високоякісна колекція всіх «стандартних» монад:

  • Списки (недетерміновані обчислення, трактуючи список як домен)
  • Можливо (обчислення, які можуть бути невдалими, але для яких звітність неважлива)
  • Помилка (обчислення, які можуть вийти з ладу і вимагають обробки винятків
  • Зчитувач (обчислення, які можуть бути представлені композиціями з простих функцій Haskell)
  • Writer (обчислення з послідовним "візуалізацією" / "веденням журналу" (до рядків, html тощо)
  • Продовження (продовження)
  • IO (розрахунки, що залежать від базової комп'ютерної системи)
  • Стан (обчислення, контекст яких містить змінне значення)

з відповідними монадними трансформаторами та класами типів. Класи типів дозволяють взаємодоповнювати підхід до об'єднання монад, об'єднуючи їхні інтерфейси, щоб конкретні монади могли реалізувати стандартний інтерфейс для монади "роду". Наприклад, модуль Control.Monad.State містить клас MonadState sm, а (State s) - примірник форми

instance MonadState s (State s) where
    put = ...
    get = ...

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

Тому:

return :: a -> m a

- це функція, яка вводить значення типу a в монадію "дії" типу m a.

(>>=) :: m a -> (a -> m b) -> m b

це функція, яка здійснює монадну дію, оцінює її результат і застосовує функцію до результату. Акуратне (>> =) те, що результат знаходиться в одній монаді. Іншими словами, в m >> = f, (>> =) витягує результат з m і прив'язує його до f, щоб результат був у монаді. (Як варіант, ми можемо сказати, що (>> =) тягне f у m і застосовує його до результату.) Як наслідок, якщо у нас f :: a -> mb і g :: b -> mc, ми можемо Дії "послідовності":

m >>= f >>= g

Або, використовуючи "do notation"

do x <- m
   y <- f x
   g y

Тип для (>>) може бути освітленим. це є

(>>) :: m a -> m b -> m b

Він відповідає оператору (;) в процедурних мовах, таких як C. Він дозволяє робити позначення типу:

m = do x <- someQuery
       someAction x
       theNextAction
       andSoOn

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

join :: m (m a) -> m a

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

newtype MaybeT m a = MaybeT { runMaybeT :: m (Maybe a) }

щоб ми могли перетворити дію в (можливоT m) в дію в m, ефективно руйнуючи шари. У цьому випадку runMaybeT :: MaybeT ma -> m (можливо a) - наш метод приєднання. (МожливоT m) - це монада, а MaybeT :: m (Можливо, а) -> MaybeT ma є ефективно конструктором нового типу дії монади в m.

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

data RandomF r a = GetRandom (r -> a) deriving Functor
type Random r a = Free (RandomF r) a


type RandomT m a = Random (m a) (m a) -- model randomness in a monad by computing random monad elements.
getRandom     :: Random r r
runRandomIO   :: Random r a -> IO a (use some kind of IO-based backend to run)
runRandomIO'  :: Random r a -> IO a (use some other kind of IO-based backend)
runRandomList :: Random r a -> [a]  (some kind of list-based backend (for pseudo-randoms))

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

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

Foo :: m (ma) <-> (m. M) a


3

Монада - це масив функцій

(Pst: масив функцій - це лише обчислення).

Насправді замість справжнього масиву (одна функція в одному масиві комірок) у вас є ці функції, пов'язані іншою функцією >> =. >> = дозволяє адаптувати результати функції i до функції i + 1, виконувати обчислення між ними або, навіть, не викликати функції i + 1.

Тут використовуються типи "типи з контекстом". Це значення з "тегом". Функції, пов'язані ланцюгом, повинні приймати "голе значення" та повертати тегований результат. Одним із обов'язків >> = є вилучення голого значення з його контексту. Існує також функція "return", яка приймає голе значення і ставить її з тегом.

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

-- a * b
multiply :: Int -> Int -> Maybe Int
multiply a b = return  (a*b)

-- divideBy 5 100 = 100 / 5
divideBy :: Int -> Int -> Maybe Int
divideBy 0 _ = Nothing -- dividing by 0 gives NOTHING
divideBy denom num = return (quot num denom) -- quotient of num / denom

-- tagged value
val1 = Just 160 

-- array of functions feeded with val1
array1 = val1 >>= divideBy 2  >>= multiply 3 >>= divideBy  4 >>= multiply 3

-- array of funcionts created with the do notation
-- equals array1 but for the feeded val1
array2 :: Int -> Maybe Int
array2 n = do
       v <- divideBy 2  n
       v <- multiply 3 v
       v <- divideBy 4 v
       v <- multiply 3 v
       return v

-- array of functions, 
-- the first >>= performs 160 / 0, returning Nothing
-- the second >>= has to perform Nothing >>= multiply 3 ....
-- and simply returns Nothing without calling multiply 3 ....
array3 = val1 >>= divideBy 0  >>= multiply 3 >>= divideBy  4 >>= multiply 3

main = do
     print array1
     print (array2 160)
     print array3

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

type MyMonad = [Int -> Maybe Int] -- my monad as a real array of functions

myArray1 = [divideBy 2, multiply 3, divideBy 4, multiply 3]

-- function for the machinery of executing each function i with the result provided by function i-1
runMyMonad :: Maybe Int -> MyMonad -> Maybe Int
runMyMonad val [] = val
runMyMonad Nothing _ = Nothing
runMyMonad (Just val) (f:fs) = runMyMonad (f val) fs

І він би використовувався так:

print (runMyMonad (Just 160) myArray1)

1
Супер-акуратно! Тож прив'язка - це лише спосіб оцінити масив функцій з контекстом, послідовно, на вході з контекстом :)
Musa Al-hassy

>>=є оператором
користувачем2418306

1
Я думаю, що аналогія "масиву функцій" не дуже роз'яснює. Якщо \x -> x >>= k >>= l >>= mце масив функцій, так це і є h . g . f, що зовсім не включає монад.
дуплод

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

3

З точки зору ОО, монада - це ємність, що вільно говорить.

Мінімальна вимога - це визначення, class <A> Somethingяке підтримує конструктор Something(A a)і хоча б один методSomething<B> flatMap(Function<A, Something<B>>)

Можливо, він також враховує, чи у вашому класі monad є які-небудь методи з підписом, Something<B> work()які зберігають правила класу - компілятор запускає в flatMap під час компіляції.

Чому корисна монада? Тому що це контейнер, який дозволяє ланцюгові операції, що зберігають семантику. Наприклад, Optional<?>зберігає семантику isPresent для Optional<String>, Optional<Integer>,Optional<MyClass> і т.д.

Як приблизний приклад,

Something<Integer> i = new Something("a")
  .flatMap(doOneThing)
  .flatMap(doAnother)
  .flatMap(toInt)

Зауважте, що ми починаємо з рядка і закінчуємо цілим числом. Дуже здорово.

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

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


2

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

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

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

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

Потік контролю зберігається, але несподівана подія надійно інкапсульована та розроблена.


2

Прості монади пояснення з тематичним дослідженням дива є тут .

Монади - це абстракції, які використовуються для функціонування залежних від послідовності функцій. Ефективно тут означає, що вони повертають тип у формі F [A], наприклад, Option [A], де Option є F, називається конструктором типу. Розглянемо це в 2 простих кроки

  1. Нижче функціональний склад транзитивний. Тож для переходу від A до CI можна скласти A => B і B => C.
 A => C   =   A => B  andThen  B => C

введіть тут опис зображення

  1. Однак якщо функція повертає тип ефекту типу Option [A], тобто A => F [B], композиція не працює, щоб перейти до B, нам потрібен A => B, але у нас A => F [B].
    введіть тут опис зображення

    Нам потрібен спеціальний оператор, "прив'язуючи", який знає, як сплавити ці функції, які повертають F [A].

 A => F[C]   =   A => F[B]  bind  B => F[C]

Функція "прив'язування" визначається для конкретного F .

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

Таким чином, ми можемо побудувати ефективну функцію A => F [B] з будь-якої чистої функції A => B ,

 A => F[B]   =   A => B  andThen  return

але даний F може також визначати власні непрозорі "вбудовані" спеціальні функції таких типів, що користувач не може їх визначити ( чистою мовою), наприклад

  • "випадковий" ( діапазон => Випадковий [Int] )
  • "друкувати" ( String => IO [()] )
  • "спробувати ... зловити" тощо.

2

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



1

Дивіться мою відповідь на запитання "Що таке монада?"

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

Він не передбачає знання функціонального програмування, і він використовує псевдокод із function(argument) := expressionсинтаксисом з найпростішими можливими виразами.

Ця програма C ++ є реалізацією монади псевдокоду. (Для довідки: Mконструктор типу, feedце операція "прив'язування" та операція wrap"повернення".)

#include <iostream>
#include <string>

template <class A> class M
{
public:
    A val;
    std::string messages;
};

template <class A, class B>
M<B> feed(M<B> (*f)(A), M<A> x)
{
    M<B> m = f(x.val);
    m.messages = x.messages + m.messages;
    return m;
}

template <class A>
M<A> wrap(A x)
{
    M<A> m;
    m.val = x;
    m.messages = "";
    return m;
}

class T {};
class U {};
class V {};

M<U> g(V x)
{
    M<U> m;
    m.messages = "called g.\n";
    return m;
}

M<T> f(U x)
{
    M<T> m;
    m.messages = "called f.\n";
    return m;
}

int main()
{
    V x;
    M<T> m = feed(f, feed(g, wrap(x)));
    std::cout << m.messages;
}

0

З практичної точки зору (узагальнюючи сказане в багатьох попередніх відповідях та пов’язаних статтях), мені здається, що однією з основних «цілей» (або корисності) монади є використання залежностей, прихованих у рекурсивному виклику методу aka композиція функції (тобто коли f1 викликає f2 виклики f3, f3 потрібно оцінити перед f2 перед f1), щоб представити послідовну композицію природним чином, особливо в контексті моделі лінивої оцінки (тобто послідовного складу як простої послідовності , наприклад, "f3 (); f2 (); f1 ();" в C - хитрість особливо очевидна, якщо ви думаєте про випадок, коли f3, f2 і f1 насправді нічого не повертають [їх ланцюг як f1 (f2 (f3)) є штучним, суто призначеним для створення послідовності]).

Це особливо актуально, коли задіяні побічні ефекти, тобто коли певний стан змінено (якщо у f1, f2, f3 не було побічних ефектів, не має значення в тому порядку, який вони оцінюються; що є чудовою властивістю чистого функціональні мови, щоб можна було, наприклад, паралелізувати ці обчислення). Чим більше чистих функцій, тим краще.

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

Це лише один аспект, як тут попереджено .


0

Найпростішим поясненням, про яке я можу придумати, є те, що монади - це спосіб складання функцій із вбудованими результатами (ака Клейслі). Функція "вбудований" має підпис, a -> (b, smth)де aі bє типи (подумайте Int, Bool), які можуть відрізнятися один від одного, але не обов'язково - іsmth є "контекстом" або "вимальовуванням".

Цей тип функцій також може бути записаний a -> m bтам, де mеквівалентно "вродженню" smth. Отже, це функції, які повертають значення в контексті (думати функції, які записують свої дії, деsmth є повідомлення журналу; або функції, які виконують введення / виведення, а їх результати залежать від результату дії IO).

Монада - це інтерфейс ("typeclass"), який змушує виконавця розповісти, як складати такі функції. Реалізатору необхідно визначити функцію композиції (a -> m b) -> (b -> m c) -> (a -> m c)для будь-якого типуmРеалізатору який хоче реалізувати інтерфейс (це композиція Kleisli).

Отже, якщо ми говоримо, що у нас є тип кортежа, що (Int, String)представляє результати обчислень на Ints, які також реєструють свої дії, при (_, String)цьому є "embelelment" - журнал дії - і дві функції, increment :: Int -> (Int, String)і twoTimes :: Int -> (Int, String)ми хочемо отримати функцію, incrementThenDouble :: Int -> (Int, String)яка є композицією двох функцій, що також враховує журнали.

У наведеному прикладі мональна реалізація двох функцій застосовується до цілого значення 2 incrementThenDouble 2(яке дорівнює twoTimes (increment 2)) повернеться (6, " Adding 1. Doubling 3.")для посередницьких результатів, increment 2рівних (3, " Adding 1.")і twoTimes 3рівним(6, " Doubling 3.")

З цієї функції композиції Клейслі можна вивести звичайні монадичні функції.

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