Перегляди №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"тег" до значення (ми просто "обчислюємо" результат, зберігаючи середовище неушкодженим).
- Закони монад (асоціативність, ідентичність) гарантуються укладачем.
Деякі примітки:
- Оскільки монадичні обчислення завжди мають тип результату
m a, не існує способу "втекти" від IOмонади. Сенс полягає в тому, що: як тільки обчислення взаємодіє із середовищем, ви не зможете побудувати обчислення з нього, яке не відбувається.
- Коли функціональний програміст не знає, як щось зробити чистим способом, він (в останню чергу) може запрограмувати завдання за допомогою певних обчислень в
IOмонаді. Ось чому IOйого часто називають кошиком гріха програміста .
- Зауважте, що в нечистому світі (у сенсі функціонального програмування) читання значення також може змінити середовище (наприклад, споживання інформації користувача). Ось чому такі функції, як
getCharповинен, мають тип результату IO something.