Різні способи побачити монаду


29

Під час навчання Haskell я зіткнувся з великою кількістю навчальних посібників, намагаючись пояснити, що таке монади і чому монади важливі в Haskell. Кожен з них використовував аналогії, щоб було легше вловити сенс. Зрештою, у мене є 3 різних точки зору того, що таке монада:

Перегляд 1: Монада як етикетка

Іноді я думаю, що монада як мітка для певного типу. Наприклад, функція типу:

myfunction :: IO Int

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

Отже, це значення Int було позначене як значення, яке надійшло від процесу з IO, тому це значення є "брудним". Ваш процес вже не є чистим.

Погляд 2: Монада як приватний простір, де можуть статися неприємні речі.

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

Вид 3: Монада як у теорії категорій

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

Чи правильні ці погляди? Що точніше?


5
№2 - це не те, що монада взагалі. Насправді це в значній мірі обмежене IO, а не корисний вигляд (пор. Що таке Монада ). Крім того, "суворим" прийнято називати властивість, яку Haskell не має (а саме суворої оцінки). До речі, і монади цього не змінюють (знову ж таки, дивіться, що таке Монада - ні).

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

У звичайного Lisp є власний компілятор як частина мови. У Haskell є Монади.
Буде Несс

Відповіді:


33

Перегляди №1 та №2 загалом неправильні.

  1. Будь-який тип даних * -> *може працювати як мітка, монади - це набагато більше, ніж це.
  2. (За винятком IOмонади) обчислення в монаді не є нечистими. Вони просто представляють обчислення, які ми сприймаємо як побічні ефекти, але вони чисті.

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

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


Стандартні обчислення

Всі обчислення в функціональному мовою програмування можна розглядати як функції з типом джерела і цільового типу: f :: a -> b. Якщо функція має більше одного аргументу, ми можемо перетворити її на функцію з одним аргументом шляхом викривлення (див. Також вікі Haskell ). І якщо у нас є тільки значення x :: a(функція з 0 аргументів), ми можемо перетворити його в функцію , яка приймає аргумент типу блоку : (\_ -> x) :: () -> a.

Ми можемо будувати більш складні програми, що утворюють простіші, складаючи такі функції за допомогою .оператора. Наприклад, якщо ми маємо f :: a -> bі g :: b -> cотримуємо g . f :: a -> c. Зауважте, що це працює і для перетворених значень: якщо ми маємо x :: aта перетворимо їх у наше представлення, ми отримаємо f . ((\_ -> x) :: () -> a) :: () -> b.

Це представлення має деякі дуже важливі властивості, а саме:

  • У нас є дуже особлива функція - функція ідентичностіid :: a -> a для кожного типу a. Це елемент ідентичності стосовно .: fдорівнює f . idі до id . f.
  • Оператор композиції функцій .є асоціативним .

Монадічні обчислення

Припустимо, ми хочемо вибрати та працювати з якоюсь особливою категорією обчислень, результат якої містить щось більше, ніж просто одне повернене значення. Ми не хочемо вказувати, що означає "щось більше", ми хочемо зберегти все якнайбільше. Найзагальніший спосіб представити "щось більше" - це представити його як типову функцію - тип mроду * -> *(тобто перетворює один тип в інший). Отже, для кожної категорії обчислень, з якими ми хочемо працювати, ми матимемо певну функцію типу m :: * -> *. (В Haskell, mє [], IO, Maybeі т.д.) І категорія містить всі функції типів a -> m b.

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

  • Мати оператора (назвемо його <=<), який складається з функцій f :: a -> m bі g :: b -> m cв щось як g <=< f :: a -> m c. І, це повинно бути асоціативним.
  • Щоб мати певну функцію ідентичності для кожного типу, давайте назвемо її return. Ми також хочемо, щоб f <=< returnце те саме, що fі те саме return <=< f.

Будь-який, m :: * -> *для якого у нас є такі функції, returnі <=<називається монадою . Це дозволяє нам створювати складні обчислення з більш простих, як і в базовому випадку, але тепер типи повернених значень перетворюються на m.

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

Монади в Хаскелл

У Haskell (та інших функціональних мовах) ми здебільшого працюємо зі значеннями, а не з функціями типів () -> a. Отже, замість визначення <=<кожної монади, ми визначаємо функцію (>>=) :: m a -> (a -> m b) -> m b. Таке альтернативне визначення рівнозначне, ми можемо виразити, >>=використовуючи <=<і навпаки (спробувати як вправу, чи побачити джерела ). Зараз принцип менш очевидний, але він залишається тим самим: наші результати завжди бувають типів, m aі ми складаємо функції типів a -> m b.

Для кожної монади, яку ми створюємо, ми не повинні забувати перевірити це returnта <=<мати необхідні нам властивості: асоціативність та ліво / право ідентичність. Виражається за допомогою returnі >>=вони називаються законами монад .

Приклад - списки

Якщо ми вирішимо mбути [], ми отримаємо категорію функцій типів a -> [b]. Такі функції представляють недетерміновані обчислення, результати яких можуть бути одним або кількома значеннями, але також не мають значення. Це породжує так звану списку монаду . Склад f :: a -> [b]і g :: b -> [c]працює наступним чином: g <=< f :: a -> [c]означає обчислити всі можливі результати типу [b], застосувати gдо кожного з них і зібрати всі результати в єдиний список. Висловлено в Haskell

return :: a -> [a]
return x = [x]
(<=<) :: (b -> [c]) -> (a -> [b]) -> (a -> [c])
g (<=<) f  = concat . map g . f

або використовуючи >>=

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

Зауважте, що в цьому прикладі типи повернення були [a]настільки можливими, що вони не містили жодного значення типу a. Дійсно, для монади немає такої вимоги, що тип повернення повинен мати такі значення. Деякі монади завжди є (як IOабо State), але деякі ні, не люблять []або Maybe.

Монада МО

Як я вже згадував, IOмонада дещо особлива. Значення типу IO aозначає значення типу, aпобудоване при взаємодії з середовищем програми. Отже (на відміну від усіх інших монад) ми не можемо описати значення типу, IO aвикористовуючи чисту конструкцію. Тут IOпросто тег або мітка, що відрізняє обчислення, які взаємодіють із середовищем. Це (єдиний випадок), коли погляди №1 та №2 є правильними.

Для IOмонади:

  • Склад f :: a -> IO bі g :: b -> IO cзасоби: обчислити , fщо взаємодіє з навколишнім середовищем, а потім обчислити, gщо використовує значення , та обчислити результат, взаємодіючи з оточенням.
  • returnпросто додає IO"тег" до значення (ми просто "обчислюємо" результат, зберігаючи середовище неушкодженим).
  • Закони монад (асоціативність, ідентичність) гарантуються укладачем.

Деякі примітки:

  1. Оскільки монадичні обчислення завжди мають тип результату m a, не існує способу "втекти" від IOмонади. Сенс полягає в тому, що: як тільки обчислення взаємодіє із середовищем, ви не зможете побудувати обчислення з нього, яке не відбувається.
  2. Коли функціональний програміст не знає, як щось зробити чистим способом, він (в останню чергу) може запрограмувати завдання за допомогою певних обчислень в IOмонаді. Ось чому IOйого часто називають кошиком гріха програміста .
  3. Зауважте, що в нечистому світі (у сенсі функціонального програмування) читання значення також може змінити середовище (наприклад, споживання інформації користувача). Ось чому такі функції, як getCharповинен, мають тип результату IO something.

3
Чудова відповідь. Я б уточнив, що IOне має особливої ​​семантики з мовної точки зору. Він не особливий, він поводиться як і будь-який інший код. Тільки реалізація бібліотеки часу виконання є особливою. Також існує спеціальний спосіб втечі ( unsafePerformIO). Я думаю, що це важливо, тому що люди часто думають про IOособливий мовний елемент або декларативний тег. Це не так.
usr

2
@usr Добре. Додам, що unsafePerformIO справді небезпечний і його слід використовувати лише фахівцям. Це дозволяє зламати все, наприклад, ви можете створити функцію, coerce :: a -> bяка перетворює будь-які два типи (і в більшості випадків завершує роботу програми). Дивіться цей приклад - ви можете перетворити навіть функцію в Intтощо.
Петро Пудлак

Ще однією "особливою магічною" монадою буде ST, яка дозволяє оголошувати посилання на пам'ять, з якої можна читати і писати, як вважаєте за потрібне (хоча тільки в межах монади), а потім ви можете отримати результат, зателефонувавшиrunST :: (forall s. GHC.ST.ST s a) -> a
sara

5

Перегляд 1: Монада як етикетка

"Отже, це значення Int було позначене як значення, яке надійшло від процесу з IO, тому це значення є" брудним "."

"IO Int" взагалі не є значенням Int (хоча це може бути в деяких випадках, таких як "return 3"). Це процедура, яка виводить деяке значення Int. Різні виконання цієї "процедури" можуть давати різні значення Int.

Монада m - це вбудована (імперативна) "мова програмування": в рамках цієї мови можна визначити деякі "процедури". Монадічне значення (типу ma) - це процедура в цій "мові програмування", яка виводить значення типу a.

Наприклад:

foo :: IO Int

це деяка процедура, яка виводить значення типу Int.

Потім:

bar :: IO (Int, Int)
bar = do
  a <- foo
  b <- foo
  return (a,b)

це певна процедура, яка виводить два (можливо, різні) інти.

Кожна така "мова" підтримує деякі операції:

  • дві процедури (ma та mb) можуть бути "об'єднані": ви можете створити більшу процедуру (ma >> mb), зроблену з першої, а потім другої;

  • тим більше, що вихід (a) першого може вплинути на другий (ma >> = \ a -> ...);

  • процедура (повернення x) може дати деяке постійне значення (x).

Різні вбудовані мови програмування відрізняються від тих видів, які вони підтримують, таких як:

  • отримання випадкових значень;
  • "вилки" ([[монада);
  • винятки (кидати / ловити) (The Either e monad);
  • явне продовження / підтримка callcc;
  • відправлення / отримання повідомлень іншим "агентам";
  • створювати, встановлювати та читати змінні (локальні для цієї мови програмування) (монада ST).

1

Не плутайте монадичний тип із класом монада.

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

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

Дивіться, що вирішує клас монади?

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