Нещодавно коротко подивившись на Haskell, що було б коротким, стислим, практичним поясненням того, що по суті є монадою?
Я знайшов більшість пояснень, які мені трапляються, досить недоступними та не мають практичних деталей.
Нещодавно коротко подивившись на Haskell, що було б коротким, стислим, практичним поясненням того, що по суті є монадою?
Я знайшов більшість пояснень, які мені трапляються, досить недоступними та не мають практичних деталей.
Відповіді:
По-перше: Термін монада трохи вакуумний, якщо ви не математик. Альтернативний термін - це обчислювач, який трохи більше описує те, для чого вони насправді корисні.
Ви запитуєте практичні приклади:
Приклад 1: Ознайомлення зі списком :
[x*2 | x<-[1..10], odd x]
Цей вираз повертає подвоєння всіх непарних чисел у межах від 1 до 10. Дуже корисно!
Виявляється, це дійсно просто синтаксичний цукор для деяких операцій в монаді Лісти. Таке ж розуміння списку можна записати як:
do
x <- [1..10]
guard (odd x)
return (x * 2)
Або навіть:
[1..10] >>= (\x -> guard (odd x) >> return (x*2))
Приклад 2: Вхід / вихід :
do
putStrLn "What is your name?"
name <- getLine
putStrLn ("Welcome, " ++ name ++ "!")
В обох прикладах використовуються монади, будівельники обчислень AKA. Поширена тема полягає в тому, що монада ланцюгує операції якимось конкретним, корисним способом. У розумінні списку операції є ланцюговими таким чином, що якщо операція повертає список, то наступні операції виконуються на кожному елементі списку. Монада вводу-виводу з іншого боку виконує операції послідовно, але передає вздовж "приховану змінну", яка представляє "стан світу", що дозволяє нам писати код вводу-виводу в чисто функціональному порядку.
Виявляється, схема ланцюгових операцій є досить корисною і використовується для безлічі різних речей в Haskell.
Інший приклад - винятки: використовуючи Error
монаду, операції є ланцюговими таким чином, що вони виконуються послідовно, за винятком випадків, коли помилка кидається, і в цьому випадку решта ланцюга відмовляється.
І синтаксис розуміння списку, і нотація є синтаксичним цукром для ланцюгових операцій за допомогою >>=
оператора. Монада - це лише тип, який підтримує >>=
оператора.
Приклад 3: Аналізатор
Це дуже простий парсер, який аналізує або цитований рядок, або число:
parseExpr = parseString <|> parseNumber
parseString = do
char '"'
x <- many (noneOf "\"")
char '"'
return (StringValue x)
parseNumber = do
num <- many1 digit
return (NumberValue (read num))
Операції char
, digit
і т.д. досить прості. Вони або відповідають, або не відповідають. Магія - це монада, яка керує контрольним потоком: Операції виконуються послідовно, поки збіг не завершиться, і в цьому випадку монада повертається до останнього <|>
і намагається наступний варіант. Знову спосіб зв’язування операцій з деякою додатковою, корисною семантикою.
Приклад 4: Асинхронне програмування
Наведені вище приклади є в Haskell, але виявляється, F # також підтримує монади. Цей приклад викрадено у « Дон Сайм» :
let AsyncHttp(url:string) =
async { let req = WebRequest.Create(url)
let! rsp = req.GetResponseAsync()
use stream = rsp.GetResponseStream()
use reader = new System.IO.StreamReader(stream)
return reader.ReadToEnd() }
Цей метод отримує веб-сторінку. Лінія перфорації - це використання GetResponseAsync
- воно фактично чекає відповіді на окремому потоці, тоді як головний потік повертається з функції. Останні три рядки виконуються на породженій нитці, коли відповідь отримана.
У більшості інших мов вам доведеться чітко створити окрему функцію для ліній, що обробляють відповідь. async
Монада в стані «розкол» блок на своєму і відкласти виконання другої половини. ( async {}
Синтаксис вказує, що керуючий потік у блоці визначається async
монадою.)
Як вони працюють
Тож як монада зможе виконати всі ці фантазійні контрольно-потокові речі? Те, що насправді відбувається в блоці do (або виразі обчислень, як їх називають у F #), - це те, що кожна операція (в основному кожен рядок) загорнута в окрему анонімну функцію. Потім ці функції поєднуються за допомогою bind
оператора (написано >>=
в Haskell). Оскільки bind
операція поєднує в собі функції, вона може виконувати їх так, як вважає за потрібне: послідовно, кілька разів, в зворотному порядку, відкинути деякі, виконати деякі на окремій нитці, коли відчуваєш, як це тощо.
Як приклад, це розширена версія IO-коду з прикладу 2:
putStrLn "What is your name?"
>>= (\_ -> getLine)
>>= (\name -> putStrLn ("Welcome, " ++ name ++ "!"))
Це гірше, але також очевидніше, що насправді відбувається. >>=
Оператор чарівний інгредієнт: Він приймає значення (на лівій стороні) і комбінує її з функцією (на правій стороні), щоб зробити нове значення. Потім це нове значення приймається наступним >>=
оператором і знову поєднується з функцією для отримання нового значення. >>=
може розглядатися як міні-оцінювач.
Зауважте, що >>=
це перевантажене для різних типів, тому кожна монада має свою реалізацію >>=
. (Усі операції в ланцюзі повинні бути типу однієї монади, інакше >>=
оператор не працюватиме.)
Найпростіша можлива реалізація >>=
просто приймає значення зліва і застосовує його до функції праворуч і повертає результат, але, як було сказано раніше, те, що робить весь шаблон корисним, це коли в монаді реалізується щось додаткове >>=
.
Існує деяка додаткова кмітливість у тому, як значення передаються від однієї операції до іншої, але для цього потрібно більш глибоке пояснення системи типу Haskell.
Підводячи підсумки
У Haskell-термінах монада - це параметризований тип, який є екземпляром класу типу Monad, який визначає >>=
поряд з кількома іншими операторами. З точки зору мирян, монада - це лише тип, для якого визначена >>=
операція.
Сам по собі >>=
є просто громіздким способом ланцюгових функцій, але за наявності до-нотації, яка приховує «сантехніку», монадічні операції виявляються дуже приємною і корисною абстракцією, корисною багатьма місцями в мові та корисною для створення власних міні-мов у мові.
Чому монади важкі?
Для багатьох учнів, які навчаються Хаскеллу, монади - це перешкода, на яку вони потрапляють, як цегляна стіна. Справа не в тому, що самі по собі монади є складними, а в тому, що реалізація покладається на багато інших вдосконалених функцій Haskell, таких як параметризовані типи, класи типів тощо. Проблема полягає в тому, що введення-виведення Haskell засноване на монадах, і введення-виведення - це, мабуть, одне з перших, що ви хочете зрозуміти, вивчаючи нову мову - адже створювати програми, які не виробляють жодної програми, не так вже й цікаво. вихід. У мене немає негайного вирішення цієї проблеми з курячим яйцем, окрім трактування вводу / виводу, як "магія відбувається тут", поки у вас не буде достатньо досвіду роботи з іншими частинами мови. Вибачте.
Чудовий блог про монади: http://adit.io/posts/2013-04-17-functors,_applicatives,_and_monads_in_pictures.html
Пояснення "що таке монада" трохи схоже на те, що сказати "що таке число?" Ми постійно використовуємо цифри. Але уявіть, що ви зустріли когось, хто нічого не знав про цифри. Як чорт ти пояснив, які цифри? І як би ви навіть почали описувати, чому це може бути корисним?
Що таке монада? Коротка відповідь: Це специфічний спосіб з'єднання операцій.
По суті, ви пишете кроки виконання та пов'язуєте їх разом із "функцією прив'язки". (У Haskell це названо >>=
.) Ви можете писати дзвінки оператору зв'язування самостійно, або ви можете використовувати синтаксичний цукор, який змушує компілятор вставити ці функції, які викликають вас. Але в будь-якому випадку кожен крок відокремлений викликом цієї функції прив’язки.
Отже функція зв’язування подібна до крапки з комою; він розділяє етапи процесу. Завдання функції зв'язування полягає в тому, щоб взяти висновок з попереднього кроку і подати його на наступний крок.
Це не звучить занадто важко, правда? Але існує кілька видів монади. Чому? Як?
Ну, функція прив'язки може просто взяти результат з одного кроку і подати його на наступний крок. Але якщо це "все", монада робить ... це насправді не дуже корисно. І це важливо для розуміння: Кожна корисна монада робить щось інше, крім того, що є лише монадою. Кожна корисна монада має "особливу силу", що робить її унікальною.
(Монада, яка не робить нічого особливого, називається "монадою ідентичності". Скоріше, як функція ідентичності, це звучить як абсолютно безглузда річ, але, як виявляється, не буває ... Але це вже інша історія ™.)
В основному, кожна монада має власну реалізацію функції зв’язування. І ви можете записати функцію прив'язки таким чином, щоб вона робила перехід між кроками виконання. Наприклад:
Якщо кожен крок повертає показник успіху / невдачі, ви можете зв'язати виконання наступного кроку лише в тому випадку, якщо попередній був успішним. Таким чином, невдалий крок припиняє всю послідовність "автоматично", без будь-якого умовного тестування від вас. ( Невдача Монада .)
Розширюючи цю ідею, можна реалізувати "винятки". ( Монада помилок або монада винятків .) Оскільки ви визначаєте їх самостійно, а не як мовну особливість, ви можете визначити, як вони працюють. (Наприклад, можливо, ви хочете проігнорувати перші два винятки і скасувати лише тоді, коли буде викинуто третє виключення.)
Ви можете зробити кожен крок повернення декількох результатів і мати цикл функції прив’язки над ними, подаючи кожен на наступний крок для вас. Таким чином, вам не доведеться тримати петлі в усьому місці, коли ви маєте справу з кількома результатами. Функція прив'язки "автоматично" робить все, що для вас. ( Список Монада .)
Окрім передачі "результату" від одного кроку до іншого, ви можете зв'язати функцію передачі додаткових даних і навколо. Ці дані тепер не відображаються у вашому вихідному коді, але ви все одно можете отримати доступ до нього з будь-якого місця, не вручну передаючи їх до кожної функції. ( Читальник Монада .)
Ви можете зробити так, щоб "додаткові дані" можна було замінити. Це дозволяє імітувати руйнівні оновлення , фактично не роблячи деструктивних оновлень. ( Держава Монада та її двоюрідний брат Письменник Монада .)
Оскільки ви тільки імітуєте руйнівні оновлення, ви можете тривіально робити те, що було б неможливо з реальними руйнівними оновленнями. Наприклад, ви можете скасувати останнє оновлення або повернути його до більш старої версії .
Ви можете зробити монаду, де обчислення можна призупинити , так що ви можете призупинити свою програму, увійти і попрацювати з внутрішніми даними про стан, а потім відновити її.
Ви можете реалізувати "продовження" як монада. Це дозволяє зламати розум людей!
Все це та багато іншого можливо з монадами. Звичайно, все це також цілком можливо без монад. Це просто різко простіше використовувати монади.
Насправді, всупереч загальному розумінню Монад, вони не мають нічого спільного з державою. Монади - це просто спосіб обгортання речей і надання способів робити операції над загорнутою речовиною, не розгортаючи її.
Наприклад, ви можете створити тип, щоб обернути ще один, у Haskell:
data Wrapped a = Wrap a
Для обгортання речі ми визначаємо
return :: a -> Wrapped a
return x = Wrap x
Щоб виконувати операції без розгортання, скажімо, у вас є функція f :: a -> b
, тоді ви можете зробити це, щоб зняти цю функцію, щоб діяти на загорнуті значення:
fmap :: (a -> b) -> (Wrapped a -> Wrapped b)
fmap f (Wrap x) = Wrap (f x)
Це про все, що є для розуміння. Однак виявляється, що для цього підйому є більш загальна функція , а саме bind
:
bind :: (a -> Wrapped b) -> (Wrapped a -> Wrapped b)
bind f (Wrap x) = f x
bind
може зробити трохи більше fmap
, але не навпаки. Власне, fmap
можна визначити лише терміни bind
і return
. Отже, визначаючи монаду .. ви даєте її тип (тут він був Wrapped a
), а потім кажете, як працює return
і bind
працює.
Прикольна річ у тому, що це виявляється такою загальною моделлю, що вона вискакує всюди, а інкапсуляція в чистому вигляді є лише однією з них.
У гарній статті про те, як монади можна використовувати для введення функціональних залежностей і, таким чином, контролювати порядок оцінювання, як це використовується в IO монаді Haskell, перегляньте IO Inside .
Що стосується розуміння монад, не хвилюйтесь над цим. Прочитайте про них те, що вам здається цікавим, і не хвилюйтесь, якщо ви відразу не зрозумієте. Тоді просто пірнати мовою на зразок Haskell. Монади - це одна з таких речей, де розуміючи трюки у мозку на практиці, одного разу ти просто раптом зрозумієш, що ти їх розумієш.
Але, ви могли б винайти Монади!
sigfpe каже:
Але всі вони представляють монади як щось езотеричне, що потребує пояснення. Але я хочу заперечити, що вони взагалі не езотеричні. Насправді, зіткнувшись з різними проблемами функціонального програмування, ви б невблаганно привели до певних рішень, які є прикладом монад. Насправді я сподіваюсь змусити вас їх винайти зараз, якщо ви ще цього не зробили. Тоді невеличкий крок, щоб помітити, що всі ці рішення насправді є маскуванням. І прочитавши це, ви, можливо, зможете зрозуміти інші документи про монади, тому що ви визнаєте все, що бачите, як щось, що ви вже вигадали.
Багато проблем, які монади намагаються вирішити, пов'язані з проблемою побічних ефектів. Тож ми почнемо з них. (Зверніть увагу, що монади дозволяють робити більше, ніж обробляти побічні ефекти. Зокрема, багато типів контейнерних об'єктів можуть розглядатися як монади. Деякі з вступів до монадів важко узгодити ці два різних способи використання монад і сконцентруватися лише на одному або інші.)
У такій імперативній мові програмування, як C ++, функції не ведуть нічого подібного до функцій математики. Наприклад, припустимо, що у нас є функція C ++, яка приймає один аргумент з плаваючою точкою і повертає результат з плаваючою точкою. Поверхово це може здатися трохи схожим на відображення математичної функції, що відповідає дійсності, але функція C ++ може зробити більше, ніж просто повернути число, яке залежить від її аргументів. Він може читати і записувати значення глобальних змінних, а також записувати вихід на екран і отримувати вхід від користувача. Однак чистою функціональною мовою функція може читати лише те, що їй подано в аргументах, і єдиний спосіб, який вона може впливати на світ, - це через значення, які вона повертає.
Монада - це тип даних, який має дві операції: >>=
(ака bind
) та return
(ака unit
). return
приймає довільне значення і створює примірник монади з ним. >>=
бере екземпляр монади і відображає функцію над нею. (Ви вже бачите, що монада є дивним видом даних, оскільки в більшості мов програмування ви не можете написати функцію, яка приймає довільне значення і створює з неї тип. Монади використовують певний параметричний поліморфізм .)
У позначеннях Haskell написано інтерфейс monad
class Monad m where
return :: a -> m a
(>>=) :: forall a b . m a -> (a -> m b) -> m b
Ці операції повинні підкорятися певним "законам", але це не страшно важливо: "закони" просто кодифікують спосіб поводження розумних реалізацій операцій (в основному, це >>=
і return
слід погодитись, як значення трансформуються в монадні екземпляри, і тобто >>=
асоціативний).
Монади стосуються не лише стану та вводу-виводу: вони абстрагують загальну схему обчислень, яка включає роботу зі станом, введення-виведення, винятки та недетермінізм. Мабуть, найпростіші монади, які можна зрозуміти, - це списки та типи варіантів:
instance Monad [ ] where
[] >>= k = []
(x:xs) >>= k = k x ++ (xs >>= k)
return x = [x]
instance Monad Maybe where
Just x >>= k = k x
Nothing >>= k = Nothing
return x = Just x
де []
і :
є конструкторами списку, ++
є оператором конкатенації, Just
і Nothing
є Maybe
конструкторами. Обидва ці монади містять загальні та корисні схеми обчислень у відповідних типах даних (зауважте, що жоден стосунок не має до побічних ефектів чи вводу / виводу).
Вам дійсно доведеться пограти, щоб написати якийсь нетривіальний код Haskell, щоб зрозуміти, про що монади і чому вони корисні.
Спершу слід зрозуміти, що таке функтор. Перед цим зрозумійте функції вищого порядку.
Функція вищого порядку - це просто функція, яка приймає функцію як аргумент.
Функтор є будь-який тип конструкції , T
для якої існує функція вищого порядку, назвемо його map
, який перетворює функцію типу a -> b
( з урахуванням будь-яких двох типів , a
а b
) в функцію T a -> T b
. Ця map
функція також повинна підкорятися законам тотожності та композиції таким чином, щоб наступні вирази поверталися істинними для всіх p
і q
(позначення Haskell):
map id = id
map (p . q) = map p . map q
Наприклад, конструктор типу, який називається, List
- це функтор, якщо він оснащений функцією типу, (a -> b) -> List a -> List b
яка підкоряється вищезазначеним законам. Очевидна єдина практична реалізація. Отримана List a -> List b
функція повторюється над заданим списком, викликаючи (a -> b)
функцію для кожного елемента і повертає список результатів.
Монада по суті тільки функтор T
з двома додатковими методами, join
, типу T (T a) -> T a
, і unit
(іноді називають return
, fork
або pure
) типу a -> T a
. Для списків у Haskell:
join :: [[a]] -> [a]
pure :: a -> [a]
Чому це корисно? Тому що ви могли, наприклад, map
над списком з функцією, яка повертає список. Join
приймає отриманий список списків і об'єднує їх. List
є монадою, оскільки це можливо.
Ви можете написати функцію , яка робить map
, то join
. Ця функція називається bind
, або flatMap
, або (>>=)
, або (=<<)
. Зазвичай в Haskell дається екземпляр монади.
Монада повинна задовольняти певним законам, а саме це join
має бути асоціативним. Це означає, що якщо у вас є значення x
типу, [[[a]]]
то воно join (join x)
повинно бути рівним join (map join x)
. І pure
має бути тотожність join
таким чином, що join (pure x) == x
.
[Відмова від відповідальності: Я все ще намагаюся повністю впорати монади. Далі - лише те, що я зрозумів досі. Якщо це неправильно, сподіваюся, хтось знаючий зателефонує мені на килим.]
Арнар написав:
Монади - це просто спосіб обгортання речей і надання способів робити операції над загорнутою речовиною, не розгортаючи її.
Саме так. Ідея іде так:
Ви берете якусь цінність і обмотуєте її якоюсь додатковою інформацією. Як і значення певного виду (наприклад, ціле число або рядок), так і додаткова інформація має певний вид.
Наприклад, що додаткова інформація може бути Maybe
або IO
.
Тоді у вас є деякі оператори, які дозволяють вам працювати з оберненими даними, переносячи цю додаткову інформацію. Ці оператори використовують додаткову інформацію для того, щоб вирішити, як змінити поведінку операції щодо завершеного значення.
Наприклад, а Maybe Int
може бути Just Int
або Nothing
. Тепер, якщо ви додасте Maybe Int
до Maybe Int
, оператор буде перевіряти , якщо вони обидва там Just Int
всередині, і якщо так, то буде розгорнути Int
с, передати їм оператор складання, повторно звернути в результаті Int
в новий Just Int
(який є дійсним Maybe Int
), і таким чином повернути a Maybe Int
. Але якщо хтось із них був Nothing
всередині, цей оператор просто негайно повернеться Nothing
, що знову ж таки є дійсним Maybe Int
. Таким чином, ви можете зробити вигляд, що ваші Maybe Int
s - це просто звичайні числа і виконувати регулярну математику на них. Якщо ви мали отримати Nothing
, ваші рівняння все одно дадуть правильний результат - без того, щоб вам було потрібно перевіряти Nothing
всюди .
Але прикладом є лише те, що відбувається Maybe
. Якщо додаткова інформація була an IO
, то IO
замість цього буде викликаний спеціальний оператор, визначений для s, і він може зробити щось зовсім інше, перш ніж виконувати додавання. (Гаразд, додавання двох IO Int
s разом, мабуть, є безглуздим - я ще не впевнений.) (Якщо ви звернули увагу на Maybe
приклад, ви помітили, що "обгортання значення додатковими речами" не завжди правильно. Але важко бути точним, правильним і точним, не будучи недовірливим.)
В принципі, "монада" приблизно означає "зразок" . Але замість книги, наповненої неофіційно поясненими та спеціально названими шаблонами, тепер у вас є мовна конструкція - синтаксис і все, що дозволяє оголошувати нові зразки як речі у вашій програмі . (Тут неточність, що всі зразки мають дотримуватися певної форми, тому монада не настільки загальна, як модель. Але я думаю, що це найближчий термін, який більшість людей знає і розуміє.)
І тому люди вважають монади настільки заплутаними: адже вони є такою родовою концепцією. Запитувати те, що робить щось монадою, так само невиразно, як запитати, що робить щось зразком.
Але подумайте про значення синтаксичної підтримки мови для ідеї шаблону: замість того, щоб читати книгу " Банда чотирьох" та запам'ятовувати побудову певного шаблону, ви просто пишете код, який реалізує цю модель в агностиці, загальний шлях один раз, і тоді ви закінчите! Потім ви можете повторно використовувати цей зразок, як-от відвідувач, стратегія, фасад або будь-який інший, просто прикрасивши з ним операції у своєму коді, не потребуючи повторної реалізації його знову і знову!
Тож саме тому люди, які розуміють монадів, вважають їх настільки корисними : це не якась концепція башти зі слонової кістки, яка інтелектуальні сноби пишаються розумінням (гаразд, це теж звичайно, трійник), а насправді робить код простішим.
M (M a) -> M a
. Справа в тому, що ви можете перетворити це на одного типу M a -> (a -> M b) -> M b
- це робить їх корисними.
Після великого прагнення я думаю, що нарешті розумію монаду. Після перечитування власної тривалої критики переважної голосової відповіді я запропоную це пояснення.
Є три питання, на які потрібно відповісти, щоб зрозуміти монади:
Як я зазначив у своїх первинних коментарях, занадто багато пояснень монади потрапляють у питання № 3, без, і перш ніж реально адекватно висвітлити питання 2 чи питання 1.
Для чого потрібна монада?
Чисті функціональні мови, такі як Haskell, відрізняються від імперативних мов, таких як C або Java тим, що чиста функціональна програма не обов'язково виконується в певному порядку, один крок за часом. Програма Haskell більше схожа на математичну функцію, в якій ви можете вирішити "рівняння" в будь-якій кількості потенційних замовлень. Це дає ряд переваг, серед яких те, що вона виключає можливість певних видів помилок, зокрема тих, що стосуються таких речей, як "держава".
Однак є певні проблеми, які не так просто вирішити за допомогою цього стилю програмування. Деякі речі, такі як консольне програмування та введення / виведення файлів, потребують того, щоб вони відбувалися у певному порядку або потребували підтримання стану. Один із способів вирішити цю проблему - створити вид об’єкта, який представляє стан обчислення, і ряд функцій, які приймають об'єкт стану як вхідний і повертають новий модифікований об'єкт стану.
Тож давайте створимо гіпотетичне значення "стану", яке представляє стан екранного консолі. як саме будується це значення, не важливо, але скажімо, це масив символів ascii довжини байтів, який представляє те, що зараз видно на екрані, і масив, який представляє останній рядок введення, введений користувачем, у псевдокоді. Ми визначили деякі функції, які приймають стан консолі, змінюють її та повертають новий стан консолі.
consolestate MyConsole = new consolestate;
Отже, щоб виконати програмування консолі, але в чистому функціональному порядку, вам потрібно буде вкласти багато функціональних викликів всередині інших.
consolestate FinalConsole = print(input(print(myconsole, "Hello, what's your name?")),"hello, %inputbuffer%!");
Програмування таким чином зберігає "чистий" функціональний стиль, при цьому змушує зміни консолі відбуватися в певному порядку. Але, ймовірно, ми хочемо зробити більше, ніж просто декілька операцій одночасно, як у наведеному вище прикладі. Функції вкладення таким чином почнуть ставати непридатними. Ми хочемо - це код, який по суті робить те саме, що і вище, але написаний трохи більше, як це:
consolestate FinalConsole = myconsole:
print("Hello, what's your name?"):
input():
print("hello, %inputbuffer%!");
Це справді був би більш зручний спосіб її написання. Як ми це зробимо?
Що таке монада?
Щойно у вас є тип (такий як consolestate
), який ви визначаєте разом з купою функцій, призначених спеціально для роботи над цим типом, ви можете перетворити весь пакет цих речей у "монаду", визначивши оператора типу :
(прив'язування), який автоматично подає значення, що повертаються зліва, в параметри функції з правого боку, а lift
оператор, який перетворює звичайні функції, у функції, що працюють з певним видом оператора зв'язування.
Як реалізується монада?
Дивіться інші відповіді, які здаються досить вільними, щоб заскочити в деталі цього.
Давши відповідь на це питання кілька років тому, я вважаю, що можу вдосконалити та спростити цю відповідь за допомогою ...
Монада - це техніка функціонування композиції, яка екстерналізує обробку для деяких сценаріїв введення, використовуючи функцію складання bind
, для попереднього введення під час композиції.
У нормальному складі функція - compose (>>)
це використання послідовно застосовувати складену функцію до результату попередника. Важливо, що складена функція потрібна для обробки всіх сценаріїв її введення.
(x -> y) >> (y -> z)
Цю конструкцію можна вдосконалити шляхом реструктуризації вкладу, щоб відповідні держави легше опитувались. Отже, замість просто y
значення може стати Mb
таким, як, наприклад, (is_OK, b)
якщо воно y
включило поняття дійсності.
Так , наприклад, коли вхід тільки можливо , число, замість того , щоб повертати рядок , яка може містити покірно містити ряд чи ні, ви могли б перебудувати тип в bool
вказує на наявність дійсного числа і число в кортежі , такі як, bool * float
. Тепер складені функції більше не потребуватимуть розбору вхідного рядка, щоб визначити, чи існує число, але може лише перевірити bool
частину кортежу.
(Ma -> Mb) >> (Mb -> Mc)
Знову ж таки, композиція відбувається природно, compose
тому кожна функція повинна обробляти всі сценарії свого введення окремо, хоча це робити зараз набагато простіше.
Однак що робити, якщо ми могли б екстерналізувати зусилля для допиту на ті часи, коли поводження зі сценарієм відбувається звичайно. Наприклад, що якщо наша програма не робить нічого , якщо вхід не в порядку , як в тому, коли is_OK
це false
. Якщо це було зроблено, тоді складеним функціям не потрібно було б самостійно обробляти цей сценарій, різко спрощуючи свій код і впливаючи на інший рівень повторного використання.
Для досягнення цієї екстерналізації ми могли б використовувати функцію bind (>>=)
, щоб виконувати composition
замість цього compose
. Таким чином, замість того, щоб просто переносити значення з виводу однієї функції на вхід іншої, Bind
слід було б оглянути M
частину Ma
та вирішити, чи слід застосовувати складену функцію до a
. Звичайно, функція bind
була б визначена спеціально для нашого конкретного M
, щоб мати можливість перевіряти її структуру та виконувати будь-який тип додатків, який ми хочемо. Тим не менш, це a
може бути чим завгодно, оскільки bind
просто передає a
несподівану складеній функції, коли вона визначає необхідність застосування. Крім того, самі складені функції більше не потрібно мати справу зM
частина вхідної структури або, спрощуючи їх. Звідси ...
(a -> Mb) >>= (b -> Mc)
або більш лаконічно Mb >>= (b -> Mc)
Коротше кажучи, монада екстерналізується і тим самим забезпечує стандартну поведінку навколо обробки певних сценаріїв входу, як тільки вхід стане розробленим для їх достатнього викриття. Ця конструкція є shell and content
моделлю, де оболонка містить дані, що стосуються застосування складеної функції, і її допитують та залишаються доступними лише bind
функції.
Тому монада - це три речі:
M
оболонка для проведення монади відповідної інформації, bind
функції реалізовані , щоб використовувати цю інформацію оболонки в її застосуванні в складі функцій до вартості контента (и) , які він знаходить всередині оболонки, і a -> Mb
створюють результати, що включають дані монадичного управління.Взагалі кажучи, вхід до функції є набагато обмежуючим, ніж її вихід, який може включати такі речі, як умови помилок; отже, Mb
структура результатів, як правило, дуже корисна. Наприклад, оператор ділення не повертає число, коли дільник 0
.
Крім того, monad
s може включати функції обтікання, які обертають значення, a
у тип монадики Ma
та загальні функції a -> b
, в монадійні функції a -> Mb
, шляхом обгортання їх результатів після застосування. Звичайно, як bind
, наприклад, такі функції обгортання специфічні для M
. Приклад:
let return a = [a]
let lift f a = return (f a)
Конструкція bind
функції передбачає незмінні структури даних та чисті функції інших речей ускладнюються і гарантій не можна давати. Таким чином, існують монадійні закони:
Дано ...
M_
return = (a -> Ma)
f = (a -> Mb)
g = (b -> Mc)
Тоді...
Left Identity : (return a) >>= f === f a
Right Identity : Ma >>= return === Ma
Associative : Ma >>= (f >>= g) === Ma >>= ((fun x -> f x) >>= g)
Associativity
означає, що bind
зберігається порядок оцінювання незалежно від того, коли bind
застосовується. Тобто, у визначенні Associativity
вище, в силу ранньої оцінки з дужки binding
з f
і g
призведе лише до функції , яка очікує Ma
в цілях завершення bind
. Отже, оцінювання Ma
має бути визначене до того, як його значення може бути застосовано f
і до цього результат, у свою чергу, застосований g
.
Фактично, монада є формою "оператора типу". Це зробить три речі. Спочатку воно "загортає" (або іншим чином перетворює) значення одного типу в інший тип (зазвичай його називають "монадичним типом"). По-друге, це зробить усі операції (або функції) доступними для базового типу, доступними для монадичного типу. Нарешті, він надасть підтримку для поєднання свого самоврядування з іншою монадою для отримання складеної монади.
"Монада", можливо, є еквівалентом "нульових типів" у Visual Basic / C #. Він приймає нерегульований тип "T" і перетворює його в "Nullable <T>", а потім визначає, що означають усі бінарні оператори під Nullable <T>.
Побічні ефекти представлені одночасно. Створюється структура, яка містить описи побічних ефектів поряд із зворотним значенням функції. Потім "підняті" операції копіюють навколо побічних ефектів, коли значення передаються між функціями.
Їх називають "монадами", а не легшими для розуміння назвою "операторів типу" з кількох причин:
(Див. Також відповіді у розділі Що таке монада? )
Хорошою мотивацією для монадів є сигфпе (Дан Піпоні), з яким ти міг би винайти монади! (І, можливо, Ви вже є) . Існує багато інших навчальних посібників з монади , багато з яких помилково намагаються пояснити монади «простими термінами», використовуючи різні аналогії: це помилка підручника монади ; уникати їх.
Як говорить ДР Маківер у " Розкажіть нам, чому ваша мова смокче :"
Отже, речі, які я ненавиджу щодо Haskell:
Почнемо з очевидного. Підручники Монади. Ні, не монади. Зокрема навчальні посібники. Вони нескінченні, роздуті і шановний бог, вони нудні. Далі я ніколи не бачив переконливих доказів того, що вони насправді допомагають. Прочитайте визначення класу, напишіть якийсь код, здолайте страшну назву.
Ви кажете, що розумієте монаду "Можливо"? Добре, ти на своєму шляху. Просто почніть використовувати інші монади і рано чи пізно ви зрозумієте, що таке монади взагалі.
[Якщо ви орієнтовані на математику, ви, можливо, захочете проігнорувати десятки навчальних посібників і вивчити їх визначення, або дотримуватися лекцій з теорії категорій :) Основна частина визначення полягає в тому, що Monad M включає "конструктор типу", який визначається для кожного існуючий тип "T" нового типу "MT", а також деякі способи переходу вперед і назад між "звичайними" типами і типами "М".]
Крім того, напрочуд, одне з найкращих вступів до монад - це фактично одна з ранніх наукових праць, що представляють монади, « Монади Філіпа Вадлера» для функціонального програмування . Насправді є практичні, нетривіальні мотивуючі приклади, на відміну від багатьох штучних навчальних посібників там.
Монади повинні контролювати потоки, які абстрактні типи даних відносяться до даних.
Іншими словами, багатьом розробникам зручно використовувати ідеї "Набори", "Списки", "Словники" (або "Хеші" або "Карти") і Дерева. У межах цих типів даних є багато спеціальних випадків (наприклад, InsertionOrderPreservingIdentityHashMap).
Однак, зіткнувшись з програмою «потік», багато розробників не зазнали багатьох інших конструкцій, ніж якщо, перемикання / випадок, робити, поки закриття goto (grr) та (можливо).
Отже, монада - це просто конструкція контрольного потоку. Кращою фразою для заміни монади було б «тип контролю».
Таким чином, монада має слоти для логіки управління, або висловлювань, або функцій - еквівалентом у структурах даних було б сказати, що деякі структури даних дозволяють додавати дані та видаляти їх.
Наприклад, монада "якщо":
if( clause ) then block
у найпростішому є два слоти - і клаузу, і блок. if
Монада, як правило , побудовані , щоб оцінити результат статті, і якщо не помилково, оцінювати блок. Багато розробників не знайомі з монадами, коли вони навчаються "якщо", і просто не потрібно розуміти монадів, щоб писати ефективну логіку.
Монади можуть ускладнитися, так само, як структури даних можуть ускладнитися, але існує багато широких категорій монад, які можуть мати схожу семантику, але різної реалізації та синтаксису.
Звичайно, таким же чином, як структури даних можуть бути повторені або пройдені, монади можуть бути оцінені.
Компілятори можуть мати або не мати підтримку визначених користувачем монад. Haskell, безумовно, робить. Ioke має деякі подібні можливості, хоча в мові не використовується термін монада.
Мій улюблений підручник Монада:
http://www.haskell.org/haskellwiki/All_ About_Monads
(із 170 000 звернень у пошуку Google для "монада"!)
@Stu: Суть монад полягає в тому, щоб додати (як правило) послідовну семантику до чистого коду; Ви навіть можете скласти монади (використовуючи Monad Transformers) та отримати більш цікаву та складну комбіновану семантику, наприклад, розбір з керуванням помилками, спільним станом та веденням журналу, наприклад. Все це можливо в чистому коді, монади просто дозволяють абстрагувати його і використовувати його в модульних бібліотеках (завжди добре в програмуванні), а також надають зручний синтаксис, щоб він виглядав обов'язковим.
У Haskell вже є перевантаження оператора [1]: він використовує класи типів так само, як можна використовувати інтерфейси в Java або C #, але Haskell просто трапляється, щоб дозволити також не алфавітно-цифрові лексеми, як + && і>, як ідентифікатори інфіксації. Це лише перевантаження оператора, як ви дивитесь на це, якщо ви маєте на увазі "перевантаження крапки з комою" [2]. Це звучить як чорна магія і вимагає неприємностей "перевантажити крапку з комою" (малюнок заповзятливих хакерів Perl отримує вітром цієї ідеї), але справа в тому, що без монад не існує крапки з комою, оскільки суто функціональний код не вимагає і не дозволяє явного послідовності.
Це все звучить набагато складніше, ніж потрібно. Стаття sigfpe досить класна, але використовує Haskell, щоб пояснити це, який тип не в змозі розірвати проблему з куркою і яйцями, коли зрозуміти Haskell для того, щоб врізати Monads і зрозуміти Monads, щоб впорати Haskell.
[1] Це окрема проблема, ніж монади, але монади використовують функцію перевантаження оператора Haskell.
[2] Це також надмірне спрощення, оскільки оператор для ланцюжка монадичних дій є >> = (вимовляється "прив'язувати"), але є синтаксичний цукор ("робити"), який дозволяє використовувати дужки та крапки з комою та / або відступ та нові рядки.
Останнім часом я по-іншому думаю про Монад. Я розглядав їх як математичне абстрагування порядку виконання , що робить можливим нові види поліморфізму.
Якщо ви використовуєте імперативну мову, і ви пишете деякі вирази по порядку, код ЗАВЖДИ працює саме в тому порядку.
І в простому випадку, коли ви використовуєте монаду, вона відчуває те саме - ви визначаєте список виразів, які відбуваються по порядку. За винятком того, що залежно від того, яку монаду ви використовуєте, ваш код може працювати в порядку (наприклад, в монаді IO), паралельно над декількома елементами одночасно (як у монаді списку), він може зупинитися наскрізь (наприклад, у монаді "Можливо") , вона може призупинити початок роботи, щоб відновитись пізніше (як у монаді відновлення), вона може перемотатися і розпочатись спочатку (як у монаді транзакцій), або може перемотати частину, щоб спробувати інші параметри (наприклад, у логіці монади) .
Оскільки монади є поліморфними, можна запускати один і той же код у різних монах, залежно від ваших потреб.
Крім того, в деяких випадках можливо поєднувати монади разом (з монадними трансформаторами), щоб отримати кілька функцій одночасно.
Я все ще новачок у монадах, але я подумав, що поділюсь посиланням, на який я знайшов, що мені дуже добре читати (З СИСТЕМИ !!): http://www.matusiak.eu/numerodix/blog/2012/3/11/ монади для непрофесійного / (без приналежності)
В основному, тепла і нечітка концепція, яку я отримала зі статті, - це концепція того, що монади - це в основному адаптери, які дозволяють розрізненим функціям працювати компонованим способом, тобто вміти поєднувати декілька функцій і змішувати їх і співставляти, не турбуючись про непослідовне повернення типи тощо. Тож функція BIND відповідає за збереження яблук з яблуками, а апельсини - з апельсинами, коли ми намагаємося зробити ці адаптери. А функція LIFT відповідає за прийняття функцій "нижчого рівня" та "модернізацію" їх для роботи з BIND-функціями, а також для їх компонування.
Я сподіваюся, що я зрозумів це правильно, і що ще важливіше, сподіваюся, що стаття має дійсне уявлення про монади. Якщо нічого іншого, ця стаття допомогла зіткнутися з моїм апетитом дізнатися більше про монади.
На додаток до чудових відповідей, наведених вище, дозвольте запропонувати вам посилання на наступну статтю (Патрік Томсон), яка пояснює монади, пов’язавши концепцію з бібліотекою JavaScript jQuery (та спосіб використання "ланцюжків методів" для маніпулювання DOM) : jQuery - це монада
Сама документація jQuery не посилається на термін "монада", але говорить про "модель побудови", яка, мабуть, більш знайома. Це не змінює того факту, що у вас є належна монада, можливо, навіть не усвідомлюючи цього.
Монади - це не метафори , а практично корисна абстракція, що випливає із загальної картини, як пояснює Даніель Співак.
Монада - це спосіб поєднання обчислень, які мають спільний контекст. Це як побудова мережі труб. При побудові мережі, через неї не протікають дані. Але коли я закінчую розбивати всі біти разом із «зв'язувати» та «повертати», я викликаю щось подібне, runMyMonad monad data
і дані протікають по трубах.
На практиці монада - це власна реалізація оператора композиції функцій, який піклується про побічні ефекти та несумісні вхідні та зворотні значення (для ланцюжка).
Якщо я правильно зрозумів, IEnumerable походить від монад. Цікаво, чи може це бути цікавим кутом підходу для нас із світу C #?
Для чого це варто, ось кілька посилань на підручники, які мені допомогли (і ні, я досі не зрозумів, що таке монади).
Дві речі, які мені найкраще допомогли, дізнавшись про це:
Глава 8, "Функціональні парсери", з книги Програмування Грема Хаттона в Haskell . Насправді це взагалі не згадує про монади, але якщо ви зможете опрацювати розділ і реально зрозуміти все, що в ньому, особливо як оцінюється послідовність операцій зв’язування, ви зрозумієте внутрішні монади. Очікуйте, що це зробить кілька спроб.
Підручник Все про монади . Це дає кілька хороших прикладів їх використання, і я повинен сказати, що аналогія в Додатку я працювала на мене.
Моноїд видається чимось таким, що забезпечує те, що всі операції, визначені на моноїді та підтримуваному типі, завжди повертатимуть підтримуваний тип усередині Monoid. Наприклад, будь-яке число + будь-яке число = число, помилок немає.
Тоді як поділ приймає два дроби і повертає дробовий, який визначає поділ на нуль як Нескінченність у haskell somewhy (що трапляється частково частинним) ...
У будь-якому випадку, видається, Monads - це лише спосіб переконатися, що ваша ланцюжок операцій поводиться передбачувано, а функція, яка претендує на Num -> Num, складена з іншою функцією Num-> Num, що називається x скажімо, вогнем ракет.
З іншого боку, якщо у нас є функція, яка запускає ракети, ми можемо скласти її з іншими функціями, які також стріляють ракетами, оскільки наш намір ясний - ми хочемо випустити ракети - але вона не буде намагатися друкувати "Привіт Світ" чомусь дивно.
У Haskell основний тип IO () або IO [()], дистрибуція дивна, і я не буду це обговорювати, але ось що я думаю:
Якщо у мене є головне, я хочу, щоб він здійснив ланцюжок дій, тому я запускаю програму, щоб створити ефект - як правило, через IO. Таким чином, я можу з'єднати операції вводу-виводу в основному разом, щоб зробити IO, більше нічого.
Якщо я спробую зробити щось, що не "повертає IO", програма поскаржиться на те, що ланцюг не тече, або в основному "Як це стосується того, що ми намагаємось зробити - дії IO", це, здається, змушує програміст тримати свій порядок думок, не відхиляючись і не думаючи про вистріл ракети, створюючи при цьому алгоритми сортування - який не протікає.
В основному, Monads, здається, є підказкою для компілятора, що "гей, ви знаєте, що ця функція повертає число тут, вона насправді не завжди працює. Іноді може створювати число, а іноді і зовсім нічого, просто тримати це в розум ". Знаючи це, якщо ви намагаєтеся стверджувати монадійну дію, монадійна дія може виступати як виняток із компіляції часу, кажучи "ей, це насправді не число, це МОЖНА бути числом, але ви не можете цього припустити, зробіть щось" щоб забезпечити прийнятність потоку ". що запобігає непередбачуваній поведінці програми - в достатній мірі.
Здається, монади не стосуються чистоти чи контролю, а збереження ідентичності категорії, за якою будь-яка поведінка передбачувана і визначена, або не складається. Ви не можете нічого робити, коли від вас очікується щось зробити, і ви не можете зробити щось, якщо від вас нічого не робити (видно).
Найбільшою причиною, про яку я міг придумати Monads, є - перегляньте Процедурний / OOP-код, і ви помітите, що ви не знаєте, звідки починається програма і не закінчується. Все, що ви бачите, - це багато стрибків і багато математики , магія та ракети. Ви не зможете його підтримувати, і якщо зможете, ви витратите досить багато часу, обертаючи свою думку навколо всієї програми, перш ніж зможете зрозуміти будь-яку її частину, адже модульність у цьому контексті базується на взаємозалежних "розділах" коду, де код оптимізований таким чином, щоб бути максимально пов'язаним для обіцянки ефективності / взаємозв'язку. Монади є дуже конкретними і чітко визначеними за визначенням, і забезпечують можливість протікання програми аналізувати та виділяти частини, які важко проаналізувати - оскільки вони самі є монадами. Монада видається " або знищити Всесвіт, або навіть спотворити час - ми не маємо уявлення і не маємо жодних гарантій того, що це таке. Монада гарантує, що це таке, що це. що дуже потужно. або знищити Всесвіт, або навіть спотворити час - ми не маємо уявлення і не маємо жодних гарантій того, що це таке. Монада гарантує, що це таке, що це. що дуже потужно.
Усі речі в "реальному світі" видаються монадами, в тому сенсі, що вони пов'язані певними законами, що дотримуються, що запобігають плутанині. Це не означає, що ми повинні імітувати всі операції цього об’єкта для створення класів, натомість ми можемо просто сказати «квадрат - це квадрат», нічого, крім квадрата, навіть прямокутника чи кола, і «квадрат має площу довжини одного з його існуючих розмірів, помножених на себе. Незалежно від того, який квадрат у вас є, якщо це квадрат у двовимірному просторі, це площа абсолютно не може бути нічого, крім довжини у квадраті, це майже тривіально довести. Це дуже потужно, оскільки нам не потрібно робити твердження, щоб переконатися, що наш світ такий, який він є, ми просто використовуємо наслідки реальності, щоб не допустити, щоб наші програми випали з колії.
Я майже гарантовано помиляюся, але я думаю, що це може допомогти комусь там, тому, сподіваємось, це комусь допоможе.
У контексті Scala ви знайдете наступне, що є найпростішим визначенням. В основному flatMap (або зв'язує) є "асоціативним" і існує ідентичність.
trait M[+A] {
def flatMap[B](f: A => M[B]): M[B] // AKA bind
// Pseudo Meta Code
def isValidMonad: Boolean = {
// for every parameter the following holds
def isAssociativeOn[X, Y, Z](x: M[X], f: X => M[Y], g: Y => M[Z]): Boolean =
x.flatMap(f).flatMap(g) == x.flatMap(f(_).flatMap(g))
// for every parameter X and x, there exists an id
// such that the following holds
def isAnIdentity[X](x: M[X], id: X => M[X]): Boolean =
x.flatMap(id) == x
}
}
Напр
// These could be any functions
val f: Int => Option[String] = number => if (number == 7) Some("hello") else None
val g: String => Option[Double] = string => Some(3.14)
// Observe these are identical. Since Option is a Monad
// they will always be identical no matter what the functions are
scala> Some(7).flatMap(f).flatMap(g)
res211: Option[Double] = Some(3.14)
scala> Some(7).flatMap(f(_).flatMap(g))
res212: Option[Double] = Some(3.14)
// As Option is a Monad, there exists an identity:
val id: Int => Option[Int] = x => Some(x)
// Observe these are identical
scala> Some(7).flatMap(id)
res213: Option[Int] = Some(7)
scala> Some(7)
res214: Some[Int] = Some(7)
ПРИМІТКА Строго кажучи, що визначення монади у функціональному програмуванні не те саме, що визначення монади в теорії категорій , яке визначається по черзі map
та flatten
. Хоча вони є певним еквівалентом за певних відображень. Ця презентація дуже хороша: http://www.slideshare.net/samthemonad/monad-presentation-scala-as-a-category
Ця відповідь починається з мотивуючого прикладу, працює через приклад, виводить приклад монади і формально визначає "монаду".
Розглянемо ці три функції в псевдокоді:
f(<x, messages>) := <x, messages "called f. ">
g(<x, messages>) := <x, messages "called g. ">
wrap(x) := <x, "">
f
приймає впорядковану пару форми <x, messages>
і повертає впорядковану пару. Він залишає перший елемент недоторканим і додає "called f. "
до другого. Те саме з g
.
Ви можете скласти ці функції та отримати своє початкове значення разом із рядком, який показує, у якому порядку виклики функцій:
f(g(wrap(x)))
= f(g(<x, "">))
= f(<x, "called g. ">)
= <x, "called g. called f. ">
Вам не подобається той факт , що f
і g
несуть відповідальність за додавання своїх власних повідомлень журналу попередньої інформації про реєстрацію. (Просто уявіть заради аргументу, що замість додавання рядків, f
а також g
повинен виконувати складну логіку на другому пункті пари. Було б болю повторити цю складну логіку у двох - і більше - різних функціях.)
Ви вважаєте за краще писати простіші функції:
f(x) := <x, "called f. ">
g(x) := <x, "called g. ">
wrap(x) := <x, "">
Але подивіться, що станеться, коли ви їх складете:
f(g(wrap(x)))
= f(g(<x, "">))
= f(<<x, "">, "called g. ">)
= <<<x, "">, "called g. ">, "called f. ">
Проблема полягає в тому, що переведення пари у функцію не дає тобі, що ти хочеш. Але що робити, якщо ви могли подати пару у функцію:
feed(f, feed(g, wrap(x)))
= feed(f, feed(g, <x, "">))
= feed(f, <x, "called g. ">)
= <x, "called g. called f. ">
Читати feed(f, m)
як "подавати m
в f
". Для того, щоб нагодувати пару <x, messages>
в функцію f
, щоб пройти x
в f
, отримати <y, message>
з f
, і повернутися <y, messages message>
.
feed(f, <x, messages>) := let <y, message> = f(x)
in <y, messages message>
Зверніть увагу на те, що відбувається, коли ви робите три функції зі своїми функціями:
По-перше: якщо ви перегорнете значення, а потім подайте отриману пару у функцію:
feed(f, wrap(x))
= feed(f, <x, "">)
= let <y, message> = f(x)
in <y, "" message>
= let <y, message> = <x, "called f. ">
in <y, "" message>
= <x, "" "called f. ">
= <x, "called f. ">
= f(x)
Це те саме, що передавати значення функції.
Друге: якщо ви годуєте пару в wrap
:
feed(wrap, <x, messages>)
= let <y, message> = wrap(x)
in <y, messages message>
= let <y, message> = <x, "">
in <y, messages message>
= <x, messages "">
= <x, messages>
Це не змінює пари.
Третє: якщо ви визначаєте функцію , яка приймає x
і подає g(x)
в f
:
h(x) := feed(f, g(x))
і подати в нього пару:
feed(h, <x, messages>)
= let <y, message> = h(x)
in <y, messages message>
= let <y, message> = feed(f, g(x))
in <y, messages message>
= let <y, message> = feed(f, <x, "called g. ">)
in <y, messages message>
= let <y, message> = let <z, msg> = f(x)
in <z, "called g. " msg>
in <y, messages message>
= let <y, message> = let <z, msg> = <x, "called f. ">
in <z, "called g. " msg>
in <y, messages message>
= let <y, message> = <x, "called g. " "called f. ">
in <y, messages message>
= <x, messages "called g. " "called f. ">
= feed(f, <x, messages "called g. ">)
= feed(f, feed(g, <x, messages>))
Це те саме, що подавати пару g
і вводити отриману пару в f
.
У вас більша частина монади. Тепер вам просто потрібно знати про типи даних у вашій програмі.
Що таке значення <x, "called f. ">
? Ну, це залежить від типу типу значення x
. Якщо x
тип t
, то для вашої пари є значення типу "пара t
і рядок". Зателефонуйте цьому типу M t
.
M
є конструктором типу: M
поодинці не посилається на тип, а M _
посилається на тип, як тільки ви заповнюєте порожню типом. An M int
- пара int та string. А M string
- пара рядків і рядків. І т.д.
Вітаємо, ви створили монаду!
Формально ваша монада - кортеж <M, feed, wrap>
.
Монада - це кортеж, <M, feed, wrap>
де:
M
- конструктор типу.feed
приймає (функція, яка приймає a t
і повертає M u
) і an M t
і повертає an M u
.wrap
бере v
і повертає M v
.t
, u
і v
будь-які три типи, які можуть бути або не бути однаковими. Монада задовольняє трьом властивостям, які ви довели для своєї конкретної монади:
Подача загорнутого t
у функцію - це те саме, що передача розгорнутої t
функції.
Формально: feed(f, wrap(x)) = f(x)
Годування M t
в wrap
комерційне підприємство не робить нічого M t
.
Формально: feed(wrap, m) = m
Подання M t
(викликати його m
) функції, яка
t
вg
M u
(називайте його n
) відg
n
вf
те саме, що
m
вg
n
відg
n
вf
Формально: feed(h, m) = feed(f, feed(g, m))
кудиh(x) := feed(f, g(x))
Як правило, feed
називається bind
(AKA >>=
в Haskell) і wrap
називається return
.
Я спробую пояснити Monad
в контексті Haskell.
У функціональному програмуванні важливе значення має склад функцій. Це дозволяє нашій програмі складатися з невеликих, легко читаються функцій.
Скажімо, у нас є дві функції: g :: Int -> String
і f :: String -> Bool
.
Ми можемо зробити (f . g) x
, що точно так само f (g x)
, x
як і Int
значення.
При складанні / застосуванні результату однієї функції до іншої важливо поєднання типів. У вищенаведеному випадку тип повернутого результату g
повинен бути таким же, як і тип, прийнятий f
.
Але іноді значення знаходяться в контекстах, і це робить трохи менш простим вирівнювання типів. (Значення в контекстах дуже корисно. Наприклад, Maybe Int
тип представляє Int
значення, яке може бути там, IO String
тип представляє String
значення, яке є в результаті виконання деяких побічних ефектів.)
Скажімо, у нас зараз є g1 :: Int -> Maybe String
і f1 :: String -> Maybe Bool
. g1
і f1
дуже схожі на g
і f
відповідно.
Ми не можемо зробити (f1 . g1) x
або f1 (g1 x)
, де x
це Int
значення. Тип результату, який повертається, g1
- це не те, що f1
очікує.
Ми могли скласти f
і g
з .
оператором, але зараз ми не можемо складати f1
і g1
з .
. Проблема полягає в тому, що ми не можемо прямо передати значення в контексті функції, яка очікує значення, яке не знаходиться в контексті.
Не було б непогано, якщо ми запровадимо оператора для складання g1
та f1
, такого, щоб ми могли писати (f1 OPERATOR g1) x
? g1
повертає значення в контексті. Значення буде виведено з контексту та застосовано до f1
. І так, у нас такий оператор. Це <=<
.
У нас також є >>=
оператор, який робить для нас абсолютно те саме, щоправда, у дещо іншому синтаксисі.
Ми пишемо: g1 x >>= f1
. g1 x
є Maybe Int
цінністю. >>=
Оператор допомагає прийняти це Int
значення з «можливо, не-там» контексту, і застосувати його до f1
. Результат f1
, який є Maybe Bool
, буде результатом всієї >>=
операції.
І нарешті, навіщо Monad
корисно? Оскільки Monad
клас типу визначає >>=
оператора, майже такий самий, як Eq
клас типу, який визначає оператори ==
та /=
.
На закінчення, Monad
клас типу визначає >>=
оператор, який дозволяє нам передавати значення в контексті (ми називаємо ці монадичні значення) функціям, які не очікують значень у контексті. Буде опікуватися контекстом.
Якщо тут слід пам’ятати одну річ, це те, Monad
що дозволити склад функції, що включає значення в контекстах .
{-# LANGUAGE InstanceSigs #-}
newtype Id t = Id t
instance Monad Id where
return :: t -> Id t
return = Id
(=<<) :: (a -> Id b) -> Id a -> Id b
f =<< (Id x) = f x
Оператор $
програми функцій
forall a b. a -> b
є канонічно визначеним
($) :: (a -> b) -> a -> b
f $ x = f x
infixr 0 $
з точки зору застосування примітивної функції Haskell f x
( infixl 10
).
Склад .
визначається в термінах $
як
(.) :: (b -> c) -> (a -> b) -> (a -> c)
f . g = \ x -> f $ g x
infixr 9 .
і задовольняє еквівалентності forall f g h.
f . id = f :: c -> d Right identity
id . g = g :: b -> c Left identity
(f . g) . h = f . (g . h) :: a -> d Associativity
.
є асоціативним і id
є його правою та лівою тотожністю.
У програмуванні монада - це конструктор типу функтора з екземпляром класу типу монада. Існує кілька рівнозначних варіантів визначення та реалізації, кожен з яких має дещо різні інтуїції щодо абстракції монади.
Функтор - це тип конструктора f
типу * -> *
з екземпляром класу типу функтор.
{-# LANGUAGE KindSignatures #-}
class Functor (f :: * -> *) where
map :: (a -> b) -> (f a -> f b)
На додаток до слідування протоколу стаціонарного типу, екземпляри класу типу функтора повинні підкорятися законам алгебраїчного функтора forall f g.
map id = id :: f t -> f t Identity
map f . map g = map (f . g) :: f a -> f c Composition / short cut fusion
Функції обчислень мають тип
forall f t. Functor f => f t
Обчислення c r
полягає в результатах r
в контексті c
.
Унарні монадні функції або стрілки Клейслі мають тип
forall m a b. Functor m => a -> m b
Стрілки Клейсі - це функції, які беруть один аргумент a
і повертають монадичні обчислення m b
.
Монади канонічно визначаються з точки зору трійки Клейслі forall m. Functor m =>
(m, return, (=<<))
реалізований як клас типу
class Functor m => Monad m where
return :: t -> m t
(=<<) :: (a -> m b) -> m a -> m b
infixr 1 =<<
Ідентичність Клейла return
є Клейл стрілок , яка сприяє значенням t
в монадичну контексті m
. Додаток Extension або Kleisli =<<
застосовує стрілку Kleisli a -> m b
до результатів обчислення m a
.
Склад Клейслі <=<
визначається з точки зору розширення як
(<=<) :: Monad m => (b -> m c) -> (a -> m b) -> (a -> m c)
f <=< g = \ x -> f =<< g x
infixr 1 <=<
<=<
складає дві стрілки Клейслі, застосовуючи стрілку ліворуч до результатів програми правої стрілки.
Екземпляри класу типу монада повинні підкорятися законам монади , найбільш елегантно викладеним з точки зору складу Клейслі:forall f g h.
f <=< return = f :: c -> m d Right identity
return <=< g = g :: b -> m c Left identity
(f <=< g) <=< h = f <=< (g <=< h) :: a -> m d Associativity
<=<
є асоціативним і return
є його правою та лівою тотожністю.
Тип особи
type Id t = t
- це функція ідентичності для типів
Id :: * -> *
Інтерпретується як функтор,
return :: t -> Id t
= id :: t -> t
(=<<) :: (a -> Id b) -> Id a -> Id b
= ($) :: (a -> b) -> a -> b
(<=<) :: (b -> Id c) -> (a -> Id b) -> (a -> Id c)
= (.) :: (b -> c) -> (a -> b) -> (a -> c)
У канонічній Хаскеллі визначається ідентичність монади
newtype Id t = Id t
instance Functor Id where
map :: (a -> b) -> Id a -> Id b
map f (Id x) = Id (f x)
instance Monad Id where
return :: t -> Id t
return = Id
(=<<) :: (a -> Id b) -> Id a -> Id b
f =<< (Id x) = f x
Тип опції
data Maybe t = Nothing | Just t
кодує обчислення, Maybe t
які не обов'язково дають результат t
, обчислення, які можуть "провалитись". Визначений варіант монади
instance Functor Maybe where
map :: (a -> b) -> (Maybe a -> Maybe b)
map f (Just x) = Just (f x)
map _ Nothing = Nothing
instance Monad Maybe where
return :: t -> Maybe t
return = Just
(=<<) :: (a -> Maybe b) -> Maybe a -> Maybe b
f =<< (Just x) = f x
_ =<< Nothing = Nothing
a -> Maybe b
застосовується до результату лише в тому випадку, якщо Maybe a
дає результат.
newtype Nat = Nat Int
Натуральні числа можна кодувати як цілі числа, що перевищують нуль.
toNat :: Int -> Maybe Nat
toNat i | i >= 0 = Just (Nat i)
| otherwise = Nothing
Натуральні числа не закриваються на відніманні.
(-?) :: Nat -> Nat -> Maybe Nat
(Nat n) -? (Nat m) = toNat (n - m)
infixl 6 -?
Опція монади охоплює основну форму обробки винятків.
(-? 20) <=< toNat :: Int -> Maybe Nat
Монада списку над типом списку
data [] t = [] | t : [t]
infixr 5 :
та його додаткова моноїдна операція "додавати"
(++) :: [t] -> [t] -> [t]
(x : xs) ++ ys = x : xs ++ ys
[] ++ ys = ys
infixr 5 ++
кодує нелінійні обчислення, [t]
даючи природну кількість 0, 1, ...
результатів t
.
instance Functor [] where
map :: (a -> b) -> ([a] -> [b])
map f (x : xs) = f x : map f xs
map _ [] = []
instance Monad [] where
return :: t -> [t]
return = (: [])
(=<<) :: (a -> [b]) -> [a] -> [b]
f =<< (x : xs) = f x ++ (f =<< xs)
_ =<< [] = []
Розширення =<<
об'єднує ++
всі списки, [b]
отримані в результаті застосування f x
стрілки Kleisli, a -> [b]
до елементів [a]
одного списку результатів [b]
.
Нехай власні подільники натурального числа n
бути
divisors :: Integral t => t -> [t]
divisors n = filter (`divides` n) [2 .. n - 1]
divides :: Integral t => t -> t -> Bool
(`divides` n) = (== 0) . (n `rem`)
тоді
forall n. let { f = f <=< divisors } in f n = []
При визначенні класу типу монада замість розширення =<<
стандарт Haskell використовує його фліп, оператор зв'язування>>=
.
class Applicative m => Monad m where
(>>=) :: forall a b. m a -> (a -> m b) -> m b
(>>) :: forall a b. m a -> m b -> m b
m >> k = m >>= \ _ -> k
{-# INLINE (>>) #-}
return :: a -> m a
return = pure
Для простоти в цьому поясненні використовується ієрархія класів типів
class Functor f
class Functor m => Monad m
У Хаскелл поточна стандартна ієрархія
class Functor f
class Functor p => Applicative p
class Applicative m => Monad m
тому що не тільки кожна монада є функціонером, але кожна програма - функтор, і кожна монада також є додатковою.
Використовуючи монаду списку, імперативний псевдокод
for a in (1, ..., 10)
for b in (1, ..., 10)
p <- a * b
if even(p)
yield p
приблизно перекладається на блок "do" ,
do a <- [1 .. 10]
b <- [1 .. 10]
let p = a * b
guard (even p)
return p
рівнозначне розуміння монади ,
[ p | a <- [1 .. 10], b <- [1 .. 10], let p = a * b, even p ]
і вираз
[1 .. 10] >>= (\ a ->
[1 .. 10] >>= (\ b ->
let p = a * b in
guard (even p) >> -- [ () | even p ] >>
return p
)
)
Позначення та розуміння монади є синтаксичним цукром для вкладених виразів зв’язування. Оператор зв'язування використовується для прив'язки локальних імен до монадичних результатів.
let x = v in e = (\ x -> e) $ v = v & (\ x -> e)
do { r <- m; c } = (\ r -> c) =<< m = m >>= (\ r -> c)
де
(&) :: a -> (a -> b) -> b
(&) = flip ($)
infixl 0 &
Визначена функція охорони
guard :: Additive m => Bool -> m ()
guard True = return ()
guard False = fail
де тип одиниці або "порожній кортеж"
data () = ()
Аддитивні монади, що підтримують вибір та невдачу, можуть бути абстраговані за допомогою класу типу
class Monad m => Additive m where
fail :: m t
(<|>) :: m t -> m t -> m t
infixl 3 <|>
instance Additive Maybe where
fail = Nothing
Nothing <|> m = m
m <|> _ = m
instance Additive [] where
fail = []
(<|>) = (++)
де fail
і <|>
утворюють моноїдforall k l m.
k <|> fail = k
fail <|> l = l
(k <|> l) <|> m = k <|> (l <|> m)
і fail
є поглинаючим / знищуючим нульовим елементом добавок монади
_ =<< fail = fail
Якщо в
guard (even p) >> return p
even p
вірно, тоді охорона виробляє [()]
і, за визначенням >>
, локальну постійну функцію
\ _ -> return p
застосовується до результату ()
. Якщо помилково, тоді охорона створює список монади fail
( []
), який не дає результату для застосування стрілки Клейслі >>
, тому це p
пропускається.
Ганебно монади використовуються для кодування обчислень.
Стан процесора є функцією
forall st t. st -> (t, st)
що переходить у стан st
і дає результат t
. Стан st
може бути що завгодно. Нічого, прапор, кількість, масив, ручка, машина, світ.
Зазвичай називають тип процесорів стану
type State st t = st -> (t, st)
Монада державного процесора - це добрий * -> *
функтор State st
. Стрілки Клейслі монади державного процесора - це функції
forall st a b. a -> (State st) b
У канонічному Haskell визначається лінива версія монади стану процесора
newtype State st t = State { stateProc :: st -> (t, st) }
instance Functor (State st) where
map :: (a -> b) -> ((State st) a -> (State st) b)
map f (State p) = State $ \ s0 -> let (x, s1) = p s0
in (f x, s1)
instance Monad (State st) where
return :: t -> (State st) t
return x = State $ \ s -> (x, s)
(=<<) :: (a -> (State st) b) -> (State st) a -> (State st) b
f =<< (State p) = State $ \ s0 -> let (x, s1) = p s0
in stateProc (f x) s1
Процесор стану запускається шляхом подачі початкового стану:
run :: State st t -> st -> (t, st)
run = stateProc
eval :: State st t -> st -> t
eval = fst . run
exec :: State st t -> st -> st
exec = snd . run
Доступ держави забезпечується примітивами get
та put
методами абстракції над державними монадами:
{-# LANGUAGE MultiParamTypeClasses, FunctionalDependencies #-}
class Monad m => Stateful m st | m -> st where
get :: m st
put :: st -> m ()
m -> st
декларує функціональну залежність типу держави st
від монади m
; наприклад, що State t
, наприклад, визначатиме тип стану t
однозначно.
instance Stateful (State st) st where
get :: State st st
get = State $ \ s -> (s, s)
put :: st -> State st ()
put s = State $ \ _ -> ((), s)
з типом блоку, що використовується аналогічно до void
C.
modify :: Stateful m st => (st -> st) -> m ()
modify f = do
s <- get
put (f s)
gets :: Stateful m st => (st -> t) -> m t
gets f = do
s <- get
return (f s)
gets
часто використовується з приладдями запису поля.
Еквівалент монади стану змінної різьби
let s0 = 34
s1 = (+ 1) s0
n = (* 12) s1
s2 = (+ 7) s1
in (show n, s2)
де s0 :: Int
, не менш референтно прозорий, але нескінченно більш елегантний та практичний
(flip run) 34
(do
modify (+ 1)
n <- gets (* 12)
modify (+ 7)
return (show n)
)
modify (+ 1)
- це обчислення типу State Int ()
, за винятком ефекту, еквівалентного return ()
.
(flip run) 34
(modify (+ 1) >>
gets (* 12) >>= (\ n ->
modify (+ 7) >>
return (show n)
)
)
Закон монади асоціативності можна записати в термінах >>=
forall m f g.
(m >>= f) >>= g = m >>= (\ x -> f x >>= g)
або
do { do { do {
r1 <- do { x <- m; r0 <- m;
r0 <- m; = do { = r1 <- f r0;
f r0 r1 <- f x; g r1
}; g r1 }
g r1 }
} }
Як і в програмі, орієнтованому на експресію (наприклад, Rust), остання заява блоку представляє його вихід. Оператор прив'язки іноді називають «програмованою крапкою з комою».
Примітиви структури управління ітерацією від структурованого імперативного програмування імітуються монадно
for :: Monad m => (a -> m b) -> [a] -> m ()
for f = foldr ((>>) . f) (return ())
while :: Monad m => m Bool -> m t -> m ()
while c m = do
b <- c
if b then m >> while c m
else return ()
forever :: Monad m => m t
forever m = m >> forever m
data World
Монада процесорів світового стану вводу / виводу - це узгодження чистого Хаскелла і реального світу, функціональної денотативної та імперативної оперативної семантики. Близький аналог реальної суворої реалізації:
type IO t = World -> (t, World)
Взаємодія сприяє нечистим примітивам
getChar :: IO Char
putChar :: Char -> IO ()
readFile :: FilePath -> IO String
writeFile :: FilePath -> String -> IO ()
hSetBuffering :: Handle -> BufferMode -> IO ()
hTell :: Handle -> IO Integer
. . . . . .
Домішка коду, що використовує IO
примітиви, постійно протоколізується системою типів. Тому що чистота є приголомшливою, те, що відбувається IO
, залишається в ній IO
.
unsafePerformIO :: IO t -> t
Або, принаймні, слід.
Підпис типу програми Haskell
main :: IO ()
main = putStrLn "Hello, World!"
розширюється до
World -> ((), World)
Функція, яка перетворює світ.
Категорія, якою є об'єкти, є типи Хаскелла, а морфізми яких функціонують між типами Хаскелла, категорія - "швидка і вільна" Hask
.
Функтор T
- це відображення від категорії C
до категорії D
; для кожного об'єкта в C
об'єкті вD
Tobj : Obj(C) -> Obj(D)
f :: * -> *
і для кожного морфізму в C
морфізмі вD
Tmor : HomC(X, Y) -> HomD(Tobj(X), Tobj(Y))
map :: (a -> b) -> (f a -> f b)
де X
, Y
є об’єкти в C
. HomC(X, Y)
є класом гомоморфізму всіх морфізмів X -> Y
у Росії C
. Функтор повинен зберігати морфізм ідентичність та композицію, "структуру" C
, в D
.
Tmor Tobj
T(id) = id : T(X) -> T(X) Identity
T(f) . T(g) = T(f . g) : T(X) -> T(Z) Composition
Категорія Клейла з категорії C
даються Клейлі трійки
<T, eta, _*>
ендофунктора
T : C -> C
( f
), морфізм ідентичності eta
( return
) та оператор розширення *
( =<<
).
Кожен марфізм Клейслі в Росії Hask
f : X -> T(Y)
f :: a -> m b
оператором розширення
(_)* : Hom(X, T(Y)) -> Hom(T(X), T(Y))
(=<<) :: (a -> m b) -> (m a -> m b)
дається морфізм у Hask
російській категорії Клейслі
f* : T(X) -> T(Y)
(f =<<) :: m a -> m b
Склад у категорії Клейслі .T
наведений у розширенні
f .T g = f* . g : X -> T(Z)
f <=< g = (f =<<) . g :: a -> m c
і задовольняє аксіоми категорії
eta .T g = g : Y -> T(Z) Left identity
return <=< g = g :: b -> m c
f .T eta = f : Z -> T(U) Right identity
f <=< return = f :: c -> m d
(f .T g) .T h = f .T (g .T h) : X -> T(U) Associativity
(f <=< g) <=< h = f <=< (g <=< h) :: a -> m d
який, застосовуючи перетворення еквівалентності
eta .T g = g
eta* . g = g By definition of .T
eta* . g = id . g forall f. id . f = f
eta* = id forall f g h. f . h = g . h ==> f = g
(f .T g) .T h = f .T (g .T h)
(f* . g)* . h = f* . (g* . h) By definition of .T
(f* . g)* . h = f* . g* . h . is associative
(f* . g)* = f* . g* forall f g h. f . h = g . h ==> f = g
з точки зору продовження канонічно подано
eta* = id : T(X) -> T(X) Left identity
(return =<<) = id :: m t -> m t
f* . eta = f : Z -> T(U) Right identity
(f =<<) . return = f :: c -> m d
(f* . g)* = f* . g* : T(X) -> T(Z) Associativity
(((f =<<) . g) =<<) = (f =<<) . (g =<<) :: m a -> m c
Монади можна також визначити не з розширенням Клейсліана, а природним перетворенням mu
у програмі, що називається join
. Монада визначається з точки зору mu
трійки над категорією C
ендофактора
T : C -> C
f :: * -> *
і дві природні трансформації
eta : Id -> T
return :: t -> f t
mu : T . T -> T
join :: f (f t) -> f t
задоволення еквівалентів
mu . T(mu) = mu . mu : T . T . T -> T . T Associativity
join . map join = join . join :: f (f (f t)) -> f t
mu . T(eta) = mu . eta = id : T -> T Identity
join . map return = join . return = id :: f t -> f t
Потім визначається клас типу монади
class Functor m => Monad m where
return :: t -> m t
join :: m (m t) -> m t
Канонічна mu
реалізація опції монада:
instance Monad Maybe where
return = Just
join (Just m) = m
join Nothing = Nothing
concat
функція
concat :: [[a]] -> [a]
concat (x : xs) = x ++ concat xs
concat [] = []
є join
монадою списку.
instance Monad [] where
return :: t -> [t]
return = (: [])
(=<<) :: (a -> [b]) -> ([a] -> [b])
(f =<<) = concat . map f
Реалізації join
можуть бути переведені з форми розширення, використовуючи еквівалентність
mu = id* : T . T -> T
join = (id =<<) :: m (m t) -> m t
Зворотний переклад від mu
форми розширення надається
f* = mu . T(f) : T(X) -> T(Y)
(f =<<) = join . map f :: m a -> m b
Філіп Вадлер: Монади функціонального програмування
Саймон Л Пейтон Джонс, Філіп Вадлер: Імперативне функціональне програмування
Джонатан М.Д. Хілл, Кіт Кларк: вступ до теорії категорій, монард теорії категорій та їх взаємозв'язок з функціональним програмуванням .
Євгеніо Модгі: Поняття обчислення та монади
Але чому теорія настільки абстрактна повинна бути корисною для програмування?
Відповідь проста: як комп'ютерні працівники, ми цінуємо абстракцію ! Коли ми розробляємо інтерфейс до програмного компонента, ми хочемо, щоб він якомога менше розкривав про реалізацію. Ми хочемо мати можливість замінити реалізацію багатьма альтернативами, багатьма іншими "екземплярами" тієї ж "концепції". Коли ми розробляємо загальний інтерфейс для багатьох бібліотек програм, ще важливішим є те, що обраний нами інтерфейс має різноманітні реалізації. Це загальне поняття монади, яке ми цінуємо так високо, тому що теорія категорій настільки абстрактна, що її концепції настільки корисні для програмування.
Тож навряд чи дивується те, що узагальнення монад, які ми представляємо нижче, також має тісний зв’язок із теорією категорій. Але ми підкреслюємо, що наша мета дуже практична: це не «впровадження теорії категорій», а пошук більш загального способу структури бібліотек-комбінаторів. Це просто наше щастя, що математики вже зробили багато роботи за нас!
від генералізації монадів до стрілок Джона Х'юза
Світ потребує чергової публікації в блозі монади, але я думаю, що це корисно для виявлення існуючих монад у дикій природі.
Наведене вище - фрактал під назвою трикутник Сєрпінського, єдиний фрактал, який я пам'ятаю намалювати. Фрактали - це самоподібна структура на зразок вищевказаного трикутника, в якій частини схожі на ціле (у цьому випадку рівно на половину шкали, як батьківський трикутник).
Монади - фрактали. Враховуючи монадичну структуру даних, її значення можуть бути складені для формування іншого значення структури даних. Ось чому це корисно для програмування, і саме тому воно виникає у багатьох ситуаціях.
http://code.google.com/p/monad-tutorial/ - це робота над вирішенням саме цього питання.
Нехай нижче " {| a |m}
" відображає частину монадичних даних. Тип даних, який рекламує a
:
(I got an a!)
/
{| a |m}
Функція, f
знає, як створити монаду, якби вона мала a
:
(Hi f! What should I be?)
/
(You?. Oh, you'll be /
that data there.) /
/ / (I got a b.)
| -------------- |
| / |
f a |
|--later-> {| b |m}
Тут ми бачимо функцію, f
намагається оцінити монаду, але її докоряють.
(Hmm, how do I get that a?)
o (Get lost buddy.
o Wrong type.)
o /
f {| a |m}
Функція,, f
знаходить спосіб витягнути це a
за допомогою >>=
.
(Muaahaha. How you
like me now!?)
(Better.) \
| (Give me that a.)
(Fine, well ok.) |
\ |
{| a |m} >>= f
Мало що f
знає, монада і >>=
в змові.
(Yah got an a for me?)
(Yeah, but hey |
listen. I got |
something to |
tell you first |
...) \ /
| /
{| a |m} >>= f
Але про що вони насправді говорять? Ну, це залежить від монади. Розмова виключно в рефераті має обмежене використання; Ви повинні мати певний досвід роботи з окремими монадами, щоб досягти розуміння.
Наприклад, тип даних Можливо
data Maybe a = Nothing | Just a
має екземпляр монади, який буде діяти наступним чином ...
Де, якщо справа Just a
(Yah what is it?)
(... hm? Oh, |
forget about it. |
Hey a, yr up.) |
\ |
(Evaluation \ |
time already? \ |
Hows my hair?) | |
| / |
| (It's |
| fine.) /
| / /
{| a |m} >>= f
Але для випадку о Nothing
(Yah what is it?)
(... There |
is no a. ) |
| (No a?)
(No a.) |
| (Ok, I'll deal
| with this.)
\ |
\ (Hey f, get lost.)
\ | ( Where's my a?
\ | I evaluate a)
\ (Not any more |
\ you don't. |
| We're returning
| Nothing.) /
| | /
| | /
| | /
{| a |m} >>= f (I got a b.)
| (This is \
| such a \
| sham.) o o \
| o|
|--later-> {| b |m}
Отже, монада Можливо дозволяє обчислити продовжуватися, якщо він насправді містить a
рекламоване, але відміняє обчислення, якщо цього немає. Однак результат все ж є частиною монадичних даних, хоча і не результатом f
. З цієї причини монада "Можливо" використовується для представлення контексту відмови.
Різні монади поводяться по-різному. Списки - це інші типи даних із монадичними екземплярами. Вони поводяться так:
(Ok, here's your a. Well, its
a bunch of them, actually.)
|
| (Thanks, no problem. Ok
| f, here you go, an a.)
| |
| | (Thank's. See
| | you later.)
| (Whoa. Hold up f, |
| I got another |
| a for you.) |
| | (What? No, sorry.
| | Can't do it. I
| | have my hands full
| | with all these "b"
| | I just made.)
| (I'll hold those, |
| you take this, and /
| come back for more /
| when you're done /
| and we'll do it /
| again.) /
\ | ( Uhhh. All right.)
\ | /
\ \ /
{| a |m} >>= f
У цьому випадку функція знала, як зробити список із свого введення, але не знала, що робити з додатковими введеннями та додатковими списками. Пов’язування >>=
, що сприяло f
поєднанню декількох результатів. Я включаю цей приклад, щоб показати, що, хоча >>=
відповідає за видобуток a
, він також має доступ до можливого пов'язаного виводу f
. Дійсно, він ніколи не витягує жодного, a
якщо не знає, що кінцевий результат має той же тип контексту.
Є й інші монади, які використовуються для представлення різних контекстів. Ось деякі характеристики ще кількох. У IO
монади насправді немає a
, але він знає хлопця і отримає це a
за вас. У State st
монади є таємна скринька, st
яку вона передасть f
під стіл, навіть незважаючи на те, що f
щойно прийшов просити a
. Reader r
Монада подібна State st
, хоча вона дозволяє тільки f
дивитися на r
.
Сенс у всьому цьому полягає в тому, що будь-який тип даних, який оголосив себе монадою, декларує якийсь контекст навколо вилучення значення з монади. Великий прибуток від усього цього? Ну, це досить просто, щоб підрахувати розрахунок з якимось контекстом. Однак це може стати безладним, коли з'єднуються декілька обчислень, завантажених контекстом. Операції монади дбають про вирішення взаємодій контексту, щоб програмісту не довелося.
Зауважте, що використання >>=
полегшує безлад, відбираючи частину автономії f
. Тобто, у наведеному вище прикладі, Nothing
наприклад, f
більше не вирішується, що робити у випадку Nothing
; він закодований >>=
. Це компроміс. Якби це було необхідно для , f
щоб вирішити , що робити в разі Nothing
, то f
повинно бути функцією від Maybe a
до Maybe b
. У цьому випадку Maybe
бути монадою не має значення.
Однак зауважте, що іноді тип даних не експортує це конструктори (дивлячись на вас IO), і якщо ми хочемо працювати з рекламованим значенням, у нас є мало вибору, окрім як працювати з його монадичним інтерфейсом.
Монада - це річ, яка використовується для інкапсуляції об'єктів, які мають мінливий стан. Найчастіше це зустрічається в мовах, які в іншому випадку не дозволяють вам змінити стан (наприклад, Haskell).
Прикладом може бути введення / виведення файлу.
Ви зможете використовувати монаду для вводу / виводу файлів, щоб виділити зміну природи стану лише коду, який використовував Monad. Код всередині Монади може ефективно ігнорувати мінливий стан світу поза Монадою - це значно спрощує міркування про загальний ефект вашої програми.