Чому в Haskell моделюються побічні ефекти як монади?


172

Чи може хтось дати деякі вказівки на те, чому нечисті обчислення в Haskell моделюються як монади?

Я маю на увазі, що монада - це просто інтерфейс з чотирма операціями, тож, що було міркуванням для моделювання побічних ефектів у ньому?


15
Монади просто визначають дві операції.
Даріо

3
а як бути з поверненням і невдачею? (окрім (>>) та (>> =))
bodacydo

55
Дві операції є returnі (>>=). x >> yте саме, що x >>= \\_ -> y(тобто ігнорує результат першого аргументу). Ми не говоримо про це fail.
porges

2
@Porges Чому б не поговорити про невдачу? Це дещо корисно в, можливо, Парсер тощо.
альтернатива

16
@monadic: failперебуває в Monadкласі через історичну аварію; воно справді належить в MonadPlus. Зверніть увагу, що його дефолтне визначення є небезпечним.
JB.

Відповіді:


292

Припустимо, функція має побічні ефекти. Якщо ми врахуємо всі ефекти, які він виробляє, як параметри входу та виходу, то функція є чистою для зовнішнього світу.

Отже, для нечистої функції

f' :: Int -> Int

ми додамо RealWorld до розгляду

f :: Int -> RealWorld -> (Int, RealWorld)
-- input some states of the whole world,
-- modify the whole world because of the side effects,
-- then return the new world.

тоді fзнову чисто. Ми визначаємо параметризований тип даних type IO a = RealWorld -> (a, RealWorld), тому нам не потрібно вводити RealWorld стільки разів, а можемо просто писати

f :: Int -> IO Int

Для програміста поводження з RealWorld безпосередньо занадто небезпечно - зокрема, якщо програміст отримує свої значення типу RealWorld, вони можуть спробувати скопіювати його, що в принципі неможливо. (Подумайте, спробуйте скопіювати, наприклад, всю файлову систему. Де б ви її помістили?) Тому наше визначення IO інкапсулює також стани всього світу.

Склад «нечистих» функцій

Ці нечисті функції марні, якщо ми не можемо зв'язати їх між собою. Розглянемо

getLine     :: IO String            ~            RealWorld -> (String, RealWorld)
getContents :: String -> IO String  ~  String -> RealWorld -> (String, RealWorld)
putStrLn    :: String -> IO ()      ~  String -> RealWorld -> ((),     RealWorld)

Ми хочемо

  • отримати ім’я файлу з консолі,
  • прочитати цей файл і
  • вивести вміст цього файлу на консоль.

Як би ми це зробили, якби ми могли отримати доступ до держав реального світу?

printFile :: RealWorld -> ((), RealWorld)
printFile world0 = let (filename, world1) = getLine world0
                       (contents, world2) = (getContents filename) world1 
                   in  (putStrLn contents) world2 -- results in ((), world3)

Ми бачимо викрійку тут. Функції називаються так:

...
(<result-of-f>, worldY) = f               worldX
(<result-of-g>, worldZ) = g <result-of-f> worldY
...

Тож ми могли б визначити оператора, ~~~який їх прив'язує:

(~~~) :: (IO b) -> (b -> IO c) -> IO c

(~~~) ::      (RealWorld -> (b,   RealWorld))
      ->                    (b -> RealWorld -> (c, RealWorld))
      ->      (RealWorld                    -> (c, RealWorld))
(f ~~~ g) worldX = let (resF, worldY) = f worldX
                   in g resF worldY

тоді ми могли б просто написати

printFile = getLine ~~~ getContents ~~~ putStrLn

не торкаючись реального світу.

"Імпуріфікація"

Тепер припустимо, що ми хочемо також зробити верхній регістр вмісту файлів. Перевищення - це чиста функція

upperCase :: String -> String

Але для того, щоб потрапити в реальний світ, він повинен повернути своє IO String. Таку функцію легко зняти:

impureUpperCase :: String -> RealWorld -> (String, RealWorld)
impureUpperCase str world = (upperCase str, world)

Це можна узагальнити:

impurify :: a -> IO a

impurify :: a -> RealWorld -> (a, RealWorld)
impurify a world = (a, world)

так що impureUpperCase = impurify . upperCase, і ми можемо писати

printUpperCaseFile = 
    getLine ~~~ getContents ~~~ (impurify . upperCase) ~~~ putStrLn

(Примітка. Зазвичай ми пишемо getLine ~~~ getContents ~~~ (putStrLn . upperCase))

Ми весь час працювали з монадами

Тепер давайте подивимося, що ми зробили:

  1. Ми визначили оператора, (~~~) :: IO b -> (b -> IO c) -> IO cякий поєднує дві нечисті функції разом
  2. Ми визначили функцію, impurify :: a -> IO aяка перетворює чисте значення в нечисте.

Тепер ми робимо ідентифікацію (>>=) = (~~~)і return = impurify, і бачимо? У нас монада.


Технічна примітка

Щоб переконатися, що це дійсно монада, є ще кілька аксіом, які також потрібно перевірити:

  1. return a >>= f = f a

     impurify a                =  (\world -> (a, world))
    (impurify a ~~~ f) worldX  =  let (resF, worldY) = (\world -> (a, world )) worldX 
                                  in f resF worldY
                               =  let (resF, worldY) =            (a, worldX)       
                                  in f resF worldY
                               =  f a worldX
  2. f >>= return = f

    (f ~~~ impurify) worldX  =  let (resF, worldY) = f worldX 
                                in impurify resF worldY
                             =  let (resF, worldY) = f worldX      
                                in (resF, worldY)
                             =  f worldX
  3. f >>= (\x -> g x >>= h) = (f >>= g) >>= h

    Зліва як вправа.


5
+1, але хочу зазначити, що це стосується конкретного випадку IO. blog.sigfpe.com/2006/08/you-could-have-invented-monads-and.html досить схожий, але узагальнюється RealWorldна… ну, побачите.
ефемія

4
Зауважте, що це пояснення насправді не може стосуватися Хаскелла IO, оскільки він підтримує взаємодію, одночасність та недетермінізм. Дивіться мою відповідь на це питання ще для деяких покажчиків.
Конал

2
@Conal GHC насправді реалізує IOцей спосіб, але RealWorldнасправді не представляє реального світу, це лише маркер, щоб підтримувати операції в порядку ("магія" - RealWorldце єдиний тип унікальності GHC Haskell)
Джеремі Лист

2
@JeremyList Як я розумію, GHC реалізує IOза допомогою комбінації цього представлення та нестандартної магії компілятора (нагадує відомий вірус С компілятора Кен Томпсон ). Для інших типів правда полягає у вихідному коді разом із звичайною семантикою Хаскелла.
Конал

1
@Clonal Мій коментар був викликаний тим, що я прочитав відповідні частини вихідного коду GHC.
Список Джеремі

43

Чи може хтось дати деякі вказівки на те, чому нечисті обчислення в Haskell моделюються як монади?

Це питання містить поширене непорозуміння. Домішка і Монада - це самостійні поняття. Домішки не моделює Монада. Скоріше, існує кілька типів даних, таких як IO, які представляють собою необхідні обчислення. А для деяких із цих типів невелика частка їх інтерфейсу відповідає шаблону інтерфейсу під назвою "Monad". Більше того, не існує жодного відомого чистого / функціонального / денотативного пояснення IO(і навряд чи воно буде одне, враховуючи мету "грішної кошика"IO ), хоча є загальновідома історія про World -> (a, World)значення цього IO a. Ця історія не може правдиво описати IO, тому щоIOпідтримує паралельність і недетермінізм. Ця історія навіть не спрацьовує, коли для детермінованих обчислень, які дозволяють взаємодія в середині обчислень зі світом.

Детальніше дивіться у цій відповіді .

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


1
@KennyTM: Але RealWorldце, як кажуть доктори , "глибоко магічно". Це маркер, який представляє те, що робить система виконання, насправді нічого не означає про реальний світ. Ви навіть не можете придумати нове, щоб зробити «нитку», не роблячи зайвих хитрощів; наївний підхід просто створив би єдину, блокувальну дію з великою неоднозначністю щодо того, коли вона запуститься.
CA McCann

4
Також я б заперечив, що монади по суті є імперативними. Якщо функтор представляє якусь структуру із вбудованими в неї значеннями, екземпляр монади означає, що ви можете будувати та вирівнювати нові шари на основі цих значень. Отже, незалежно від значення, яке ви присвоюєте одному шару функтора, монада означає, що ви можете створити необмежену кількість шарів із суворим поняттям причинності, що переходять від одного до іншого. Конкретні випадки можуть не мати внутрішньо імперативної структури, але Monadв цілому це дійсно так.
CA McCann

3
Під " Monadзагалом" я маю на увазі приблизно forall m. Monad m => ..., тобто працюю над довільною інстанцією. Те, що ви можете зробити з довільною монадою, - це майже те саме, що ви можете робити IO: отримувати непрозорі примітиви (як аргументи функцій або відповідно з бібліотек), конструювати безвідмовні операції returnабо перетворювати значення незворотно, використовуючи (>>=). Суть програмування у довільній монаді полягає у формуванні списку безповоротних дій: "зробіть X, тоді зробіть Y, тоді ...". Звучить для мене досить імперативно!
CA McCann

2
Ні, ви все ще пропускаєте тут мою думку. Звичайно, ви б не використовували цей спосіб мислення для жодного з цих конкретних типів, оскільки вони мають чітку, змістовну структуру. Коли я кажу "довільні монади", я маю на увазі "ви не можете вибрати, яку"; перспектива тут знаходиться зсередини квантора, тому мислення mяк екзистенціалу може бути кориснішим. Крім того, моя "інтерпретація" - це перефразовування законів; перелік висловлювань "do X" - це саме вільний моноїд на невідомій структурі, створеній через (>>=); а закони монади - це лише моноїдні закони про склад ендофактора.
CA McCann

3
Коротше кажучи, найбільшою нижньою межею того, що описують усі монади разом, є сліпий безглуздий похід у майбутнє. IOє патологічним випадком саме тому, що він не пропонує майже нічого іншого, ніж цей мінімум. У конкретних випадках типи можуть виявляти більшу структуру і, таким чином, мати фактичне значення; але в іншому випадку основні властивості монади - засновані на законах - такі ж антитетичні, як і чітке позначення IO. Без експорту конструкторів, вичерпного перерахування примітивних дій чи чогось подібного ситуація є безнадійною.
CA McCann

13

Як я це розумію, хтось на ім’я Євгеніо Модгі вперше зауважив, що раніше незрозуміла математична конструкція під назвою "монада" може використовуватися для моделювання побічних ефектів у комп'ютерних мовах, а отже, уточнюючи їх семантику за допомогою обчислення Ламбди. Коли Хаскелл розроблявся, існували різні способи моделювання нечистих обчислень (детальніше див. Папір "волосся сорочки" Саймона Пейтона Джонса ), але коли Філ Вейдлер представив монади, швидко стало очевидним, що це відповідь. А решта - це історія.


3
Не зовсім. Відомо, що монада може моделювати інтерпретацію дуже довго (принаймні, з часу "Топой: категоричний аналіз логіки"). З іншого боку, неможливо було чітко виразити типи для монад, поки сильно не набрали функціоналу мови розійшлися, і тоді Модгі зібрав два і два разом
номен.

1
Можливо, монадам можна було б легше зрозуміти, якби вони були визначені з точки зору обгортання карти та розкручування, а повернення є синонімом обертання.
aoeu256

9

Чи може хтось дати деякі вказівки на те, чому нечисті обчислення в Haskell моделюються як монади?

Добре, тому що Haskell чистий . Вам потрібна математична концепція, щоб розрізняти нечисті обчислення та чисті на рівні типу та моделювати потоки програм відповідно.

Це означає, що вам доведеться створити якийсь тип, IO aякий моделює нечисті обчислення. Тоді вам потрібно знати способи комбінування цих обчислень, які застосовуються в послідовності ( >>=) і зняття значення ( return) є найбільш очевидними та основними.

З цими двома ви вже визначили монаду (навіть не думаючи про це);)

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

Додаткову інформацію див. У монадах у функціональному програмуванні та унікальній типізації (єдина відома мені альтернатива).


6

Як ви кажете, Monadце дуже проста структура. Половина відповіді: Monadце найпростіша структура, яку ми могли б надати побічним функціям і мати можливість їх використовувати. З Monadми можемо зробити дві речей: ми можемо розглядати чисте значення як значення бокового здійснення ( return), і ми можемо застосувати функцію бічного здійснення до величини бічного здійснення , щоб отримати нове бічне здійснення значення ( >>=). Втрата здатності робити будь-яку з цих речей було б каліцтвом, тому наш побічний тип повинен бути «принаймні» Monad, і виявляється, що Monadдостатньо для реалізації всього, що нам було потрібно до цього часу.

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

Гаразд, що ж ми можемо сказати про цю операцію? Це асоціативно; тобто, якщо ми поєднуємо три побічні ефекти, не має значення, в якому порядку ми робимо комбінування. Якщо ми робимо (записуємо файл, то читаємо сокет), тоді вимикаємо комп'ютер, це те саме, що робити файл запису тоді (читати сокет, потім вимикати) комп’ютер). Але це не комутативно: ("написати файл", потім "видалити файл") є іншим побічним ефектом від ("видалити файл", потім "написати файл"). І у нас є ідентичність: діє особливий побічний ефект "немає побічних ефектів" ("немає побічних ефектів", потім "видалити файл" - це той же побічний ефект, що і просто "видалити файл"). У цей момент будь-який математик думає "Група!" Але у груп є обертання, і взагалі немає способу перевернути побічний ефект; "видалити файл" є незворотним. Отже, структура, яку ми залишили, - це моноїд, а це означає, що наші побічні функції повинні бути монадами.

Чи є більш складна структура? Звичайно! Ми могли б розділити можливі побічні ефекти на файлові системи, ефекти на основі мережі та багато іншого, і ми могли б розробити більш детальні правила композиції, які зберегли ці деталі. Але знову ж це зводиться до: Monadдуже простий і в той же час досить потужний, щоб висловити більшість властивостей, які нас цікавлять. (Зокрема, асоціативність та інші аксіоми дозволяють перевірити наше застосування невеликими шматочками, впевнено, що побічні ефекти комбінованого застосування будуть такими ж, як і комбінація побічних ефектів штук).


4

Це насправді досить чистий спосіб думати про введення / виведення функціонально.

У більшості мов програмування ви виконуєте операції введення / виводу. У Haskell уявіть, як писати код не для виконання операцій, а для створення списку операцій, які ви хотіли б зробити.

Монади - це просто синтаксис саме для цього.

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


3

AFAIK, причина полягає в тому, що можна включити перевірку побічних ефектів у систему типів. Якщо ви хочете дізнатися більше, прослухайте ці епізоди SE-Radio : Епізод 108: Саймон Пейтон Джонс про функціональне програмування та Хаскелл Епізод 72: Ерік Мейєр на LINQ


2

Вище є дуже хороші детальні відповіді з теоретичним підґрунтям. Але я хочу висловити своє уявлення про монаду IO. Я не досвідчений програміст haskell, тому, можливо, це досить наївно чи навіть неправильно. Але я допоміг мені певною мірою боротися з монадою IO (зауважте, що це не стосується інших монад).

По-перше, я хочу сказати, що приклад із "реальним світом" для мене не надто зрозумілий, оскільки ми не можемо отримати доступ до його попереднього стану (реального світу). Можливо, це взагалі не стосується обчислень монад, але це бажано в сенсі референтної прозорості, яка, як правило, представлена ​​в коді haskell.

Тому ми хочемо, щоб наша мова (haskell) була чистою. Але нам потрібні операції введення / виводу, оскільки без них наша програма не може бути корисною. І ці операції за своєю природою не можуть бути чистими. Тож єдиним способом впоратися з цим ми повинні відокремити нечисті операції від решти коду.

Тут приходить монада. Насправді я не впевнений, що не може існувати інша конструкція з подібними необхідними властивостями, але справа в тому, що монада має ці властивості, тому її можна використовувати (і вона успішно використовується). Основна властивість полягає в тому, що ми не можемо від цього втекти. Інтерфейс Monad не має операцій, щоб позбутися монади навколо нашої вартості. Інші монади (не IO) забезпечують такі операції і дозволяють узгоджувати візерунок (наприклад, можливо), але ці операції не в інтерфейсі монади. Ще одна необхідна властивість - це можливість здійснювати ланцюгові операції.

Якщо ми думаємо про те, що нам потрібно з точки зору системи типів, ми приходимо до того, що нам потрібен тип з конструктором, який можна обернути навколо будь-якої долі. Конструктор повинен бути приватним, оскільки ми забороняємо виходити з нього (тобто відповідність шаблону). Але нам потрібна функція, щоб поставити значення в цей конструктор (тут повернення приходить на думку). І нам потрібен шлях до ланцюгових операцій. Якщо ми будемо думати про це деякий час, то дійдемо до того, що операція ланцюга повинна мати тип, як >> = має. Отже, ми приходимо до чогось дуже схожого на монаду. Я думаю, якщо зараз проаналізувати можливі суперечливі ситуації з цією конструкцією, ми прийдемо до аксіом монади.

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

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

Нарешті, я хочу сказати, що монада не перетворює нечисті операції в чисті. Це дозволяє лише ефективно їх розділити. (Я повторюю, що це лише моє розуміння)


1
Вони допомагають ввести перевірку програми, дозволяючи вводити ефекти перевірки, і ви можете визначити власні DSL, створивши монади, щоб обмежити ефекти, які можуть виконувати ваші функції, і компілятор може перевірити ваші помилки послідовності.
aoeu256

Цей коментар від aoeu256 - це "чому", якого не вистачає у всіх поясненнях, що даються до цього часу. (тобто: монади не для людей, а для упорядників)
Жоао Отеро
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.