Перегляди №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
.