Чи користь схеми монад МО для лікування побічних ефектів суто академічна?


17

Вибачте за ще одне питання щодо FP + побічних ефектів, але я не зміг знайти існуючого, який цілком відповів би на мене.

Моє (обмежене) розуміння функціонального програмування полягає в тому, що стан / побічні ефекти повинні бути мінімізовані і триматися окремо від логіки без стану.

Я також збираю підхід Хаскелла до цього, монада IO, досягає цього шляхом загортання стаціонарних дій у контейнер для подальшого виконання, розглянутих поза межами самої програми.

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

Сирий приклад вхід.

Якщо моя програма перетворює XML-файл у файл JSON:

def main():
    xml_data = read_file('input.xml')  # impure
    json_data = convert(xml_data)  # pure
    write_file('output.json', json_data) # impure

Чи не ефективний підхід МО монад для цього:

steps = list(
    read_file,
    convert,
    write_file,
)

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

Або по-іншому, це як писати:

def main():  # pure
    def inner():  # impure
        xml_data = read_file('input.xml')
        json_data = convert(xml_data)
        write_file('output.json', json_data)
    return inner

то очікуєте, що хтось інший зателефонує inner()та скаже, що ваша робота виконана, оскільки main()це чисто.

У цілому програма, в основному, міститься в монаді IO, в основному.

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

Я цілком вдячний на користь скорочення та ізоляції поведінки, що склалася, тому я фактично побудував імперативну версію на зразок такої: збирайте матеріали, чисті речі, випивайте результати. Сподіваємось, ви convert()можете бути абсолютно чистими і скористатися перевагами керованості, безпекою ниток тощо.

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

Чи є якась додаткова користь для боротьби з побічними ефектами, яку приносить модель МО монади, якої я не вистачає?


1
Ви повинні подивитися це відео . Чудеса монад нарешті розкриваються, не вдаючись до Теорії категорій чи Хаскелла. Виявляється, монади тривіально виражаються в JavaScript і є одним з ключових факторів Ajax. Монади дивовижні. Це прості речі, майже тривіально реалізовані, з величезною силою управління складністю. Але зрозуміти їх напрочуд складно, і більшість людей, як тільки вони отримали цей момент а-ха, здається, втрачають здатність пояснювати їх іншим.
Роберт Харві

Гарне відео, дякую. Я фактично дізнався про цей матеріал від впровадження JS до функціонального програмування (тоді прочитав мільйон більше…). Хоча подивившись це, я впевнений, що моє питання стосується монади IO, яку Крок не висвітлює у цьому відео.
Стю Кокс

Гм ... Хіба AJAX не вважається формою вводу / виводу?
Роберт Харві

1
Зауважте, що тип mainпрограми Haskell IO ()- це дія вводу-виводу. Це насправді зовсім не функція; це цінність . Уся ваша програма - це чисте значення, що містить вказівки, які повідомляють мові виконання, що вона повинна робити. Всі нечисті речі (фактично виконуючи дії IO) виходять за рамки вашої програми.
Wyzard

У вашому прикладі монадійна частина - це коли ви берете результат одного обчислення ( read_file) і використовуєте його як аргумент до наступного ( write_file). Якби у вас була лише послідовність незалежних дій, вам не знадобиться монада.
lortabac

Відповіді:


14

У цілому програма, в основному, міститься в монаді IO, в основному.

Це те, де я думаю, ви не бачите цього з точки зору Haskellers. Отже, у нас є така програма:

module Main

main :: IO ()
main = do
  xmlData <- readFile "input.xml"
  let jsonData = convert xmlData
  writeFile "output.json" jsonData

convert :: String -> String
convert xml = ...

Я думаю, що типовим заходом Хаскеллера є це convert, чиста частина:

  1. Мабуть, основна частина цієї програми і набагато складніша за IOчастини;
  2. Можна обмірковувати і перевіряти, не маючи справи з цим IOвзагалі.

Тому вони не бачать в цьому convertбути «містяться» в IO, а, як це бути ізольовані від IO. Від його типу, що завгодно convertніколи не може залежати від того, що відбувається в IOдії.

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

Я б сказав, що це розпадається на дві речі:

  1. Коли програма запускається, значення аргументу до convertзалежить від стану файлу.
  2. Але те, що convertфункція робить , це не залежить від стану файлу. convertзавжди одна і та ж функція , навіть якщо вона викликається різними аргументами в різних точках.

Це дещо абстрактний момент, але насправді ключовим є те, що мають на увазі Haskellers, коли вони говорять про це. Ви хочете записати convertтаким чином, що, даючи будь-який дійсний аргумент, він дасть правильний результат для цього аргументу. Якщо ви дивитесь на це так, той факт, що читання файлу є оперативною операцією, не входить у рівняння; Важливо лише те, що будь-який аргумент подається до нього і де б це не було, він convertповинен правильно поводитися. А той факт, що чистота обмежує те, що convertможна зробити з її вкладом, спрощує це міркування.

Отже, якщо convertз деяких аргументів виробляються неправильні результати і readFileподається такий аргумент, ми не бачимо це як помилку, яку вводить держава . Це помилка в чистій функції!


Я думаю, що це найкращий опис (хоча інші допомогли прояснити речі і для мене), дякую.
Стю Кокс

чи варто зауважити, що використання монад у python може мати меншу користь, оскільки python має лише один (статичний) тип, а отже, не мати жодних гарантій ні про що?
jk.

7

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

Як пояснено в « Справі з незручним загоном » Саймона Пейтона Джонса ( настійно рекомендується читати!), Монадічний введення-вивід мав на меті вирішити реальні проблеми з тим, як Haskell використовував для обробки вводу-виводу. Прочитайте приклад сервера із запитами та відповідями, який я не копіюю тут; це дуже повчально.

Haskell, на відміну від Python, заохочує стиль "чистого" обчислення, який може бути застосований системою його типів. Звичайно, ви можете використовувати самодисципліну під час програмування на Python, щоб відповідати цьому стилю, але як бути з модулями, які ви не написали? Без особливої ​​допомоги системи типів (і загальних бібліотек) монадічний введення / виведення, мабуть, менш корисний у Python. Філософія мови просто не призначена для забезпечення суворого чистого / нечистого роз'єднання.

Зауважте, що це говорить більше про різні філософії Хаскелла та Питона, ніж про те, наскільки академічний монадичний вхід / вивід. Я б не використовував його для Python.

Ще одна річ. Ти кажеш:

У цілому програма, в основному, міститься в монаді IO, в основному.

Це правда, що mainфункція Haskell "живе" IO, але реальні програми Haskell рекомендується не використовувати, IOколи це не потрібно. Практично кожна функція, яку ви пишете, яку не потрібно виконувати, вводу / виводу не повинна мати тип IO.

Тому я б сказав, що у вашому останньому прикладі ви отримали це назад: mainнечисто (тому що він читає і записує файли), але основні функції, такі convertяк чисті.


3

Чому ІО нечистий? Тому що він може повертати різні значення в різний час. Існує залежність від часу, яку необхідно враховувати, так чи інакше. Це ще важливіше при лінивій оцінці. Розглянемо наступну програму:

main = do  
    putStrLn "Please enter your name"  
    name <- getLine
    putStrLn $ "Hello, " ++ name

Без монади IO, чому б перший підказки коли-небудь отримував вихід? Від цього немає нічого, тому лінива оцінка означає, що вона ніколи не затребувана. Також немає нічого переконливого, щоб підказка виводилася до зчитування вводу. Що стосується комп'ютера, без монади IO, ці перші два вирази абсолютно не залежать один від одного. На щастя, nameнакладає наказ на двох інших.

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


2

Моє (обмежене) розуміння функціонального програмування полягає в тому, що стан / побічні ефекти повинні бути мінімізовані і триматися окремо від логіки без стану.

Це не просто функціональне програмування; зазвичай це гарна ідея на будь-якій мові. Якщо ви модульного тестування, як ви відокремлені один від одного read_file(), convert()і write_file()приходить цілком природно , тому що, незважаючи на convert()час, безумовно , найскладнішою і самої великої частини коду, написання тестів для нього відносно легко: все , що вам потрібно налаштувати це вхідний параметр . Писати тести для read_file()і write_file()досить складніше (навіть незважаючи на те, що самі функції майже тривіальні), оскільки вам потрібно створити та / або прочитати речі у файловій системі до та після виклику функції. В ідеалі ви б зробили такі функції настільки простими, що вам буде комфортно не перевіряти їх і, таким чином, заощадити багато клопоту.

Різниця між Python і Haskell тут полягає в тому, що Haskell має перевірку типу, яка може довести, що функції не мають побічних ефектів. У Python потрібно сподіватися, що ніхто не випадково не потрапив у функцію читання файлів чи написання файлів у convert()(скажімо, read_config_file()). У Haskell, коли ви заявляєте convert :: String -> Stringабо подібне, без IOмонади, перевірка типу гарантує, що це чиста функція, яка покладається лише на вхідний параметр і більше нічого. Якщо хтось спробує модифікувати convertчитання файлу конфігурації, він швидко побачить помилки компілятора, що показують, що вони порушують чистоту функції. (І , сподіваюся , що вони будуть досить розумною , щоб перейти read_config_fileз convertі передати його результат в convertпідтримці чистоти.)

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.