Перше, що слід усвідомити щодо продовження монади, це те, що, по суті, це насправді взагалі нічого не робить . Це правда!
Основна ідея продовження загалом полягає в тому, що воно представляє решту обчислень . Скажімо , у нас є такий вислів , як це: foo (bar x y) z
. Тепер витягніть лише частину в дужках, bar x y
--це частина загального виразу, але це не просто функція, яку ми можемо застосувати. Замість цього, це те , що нам потрібно застосувати функцію до . Отже, ми можемо говорити про "решту обчислень" у даному випадку як про те \a -> foo a z
, що ми можемо застосувати bar x y
для реконструкції повної форми.
Зараз трапляється, що ця концепція "решти обчислень" корисна, але з нею незручно працювати, оскільки це щось поза підвиразом, який ми розглядаємо. Для того, щоб зробити речі краще працювати, ми можемо перетворити речі навиворіт: екстракт підвираз ми зацікавлені в тому , а потім оберніть його в функцію , яка приймає аргумент , який представляє інші обчислення: \k -> k (bar x y)
.
Ця модифікована версія надає нам велику гнучкість - вона не тільки витягує підвираз із свого контексту, але й дозволяє нам маніпулювати цим зовнішнім контекстом у самому підвиразі . Ми можемо сприймати це як своєрідне призупинене обчислення , що дає нам явний контроль над тим, що відбувається далі. Тепер, як ми могли б це узагальнити? Ну, підвираз майже незмінний, тож давайте просто замінимо його параметром на функцію \x k -> k x
вивороту , надаючи нам - іншими словами, не що інше, як додаток функції, зворотне . Ми могли б так само легко написати flip ($)
або додати трохи екзотичного смаку іноземної мови та визначити його як оператора |>
.
Тепер перекласти кожен фрагмент виразу в цю форму було б просто, хоч і нудно і жахливо заплутано. На щастя, є кращий спосіб. Як програмісти Haskell, коли ми думаємо будувати обчислення у фоновому контексті, наступне, що, на наш погляд, це сказати, це монада? І в цьому випадку відповідь - так , так.
Щоб перетворити це на монаду, ми почнемо з двох основних будівельних блоків:
- Для монади
m
значення типу m a
представляє наявність доступу до значення типу a
в контексті монади.
- Основою наших "призупинених обчислень" є перевернутий додаток функцій.
Що означає мати доступ до чогось типу a
в цьому контексті? Це просто означає , що для деякого значення x :: a
, ми застосували flip ($)
до x
, що дає нам функцію , яка приймає функцію , яка приймає аргумент типу a
, і застосовує цю функцію x
. Скажімо, у нас призупинено обчислення, що містить значення типу Bool
. Який тип це нам дає?
> :t flip ($) True
flip ($) True :: (Bool -> b) -> b
Тож для призупинених обчислень тип m a
працює на (a -> b) -> b
... що, мабуть, є антиклімаксом, оскільки ми вже знали підпис Cont
, але поки що мені сподобатися.
Цікаво відзначити, що своєрідний "розворот" також застосовується до типу монади: Cont b a
представляє функцію, яка приймає функцію a -> b
та обчислює b
. Як продовження представляє "майбутнє" обчислення, так і тип a
у підписі представляє в якомусь сенсі "минуле".
Отже, замінивши (a -> b) -> b
на Cont b a
, який монадичний тип для нашого основного будівельного блоку програми зворотних функцій? a -> (a -> b) -> b
перекладається на a -> Cont b a
... підпис того ж типу, що return
і, власне, це саме те, що воно є.
З цього моменту все майже випадає безпосередньо з типів: По суті, немає розумного способу реалізації, >>=
крім фактичної реалізації. Але що це насправді робить ?
На цьому етапі ми повертаємось до того, що я сказав спочатку: продовження монади насправді нічого не робить . Щось типу Cont r a
тривіально еквівалентно чомусь просто типу a
, просто подаючи id
як аргумент призупиненого обчислення. Це може змусити запитати, якщо Cont r a
монада, але перетворення настільки тривіальна, чи не повинно a
поодинці також бути монадою? Звичайно, це не працює так, як є, оскільки немає конструктора типу, який можна визначити як Monad
екземпляр, але, скажімо, ми додаємо тривіальну обгортку, наприклад data Id a = Id a
. Це справді монада, а саме монада ідентичності.
Що робить >>=
для ідентичності монади? Підпис типу є Id a -> (a -> Id b) -> Id b
, що еквівалентно a -> (a -> b) -> b
, що є просто простою функціональною програмою знову. Встановивши, що Cont r a
тривіально еквівалентно Id a
, ми можемо зробити висновок, що і в цьому випадку (>>=)
це просто застосування функції .
Звичайно, Cont r a
це божевільний перевернутий світ, де у кожного є козли, тож те, що насправді відбувається, включає перемішування речей навколо неясними способами, щоб об’єднати два призупинених обчислення в нове призупинене обчислення, але по суті, насправді не відбувається нічого незвичного на! Застосування функцій до аргументів, хо хом, ще один день у житті функціонального програміста.