Якщо функціональні мови програмування не можуть врятувати будь-який стан, як вони виконують такі прості речі, як читання вводу від користувача (я маю на увазі, як вони їх "зберігають"), або зберігання будь-яких даних з цього приводу?
Як ви вже зрозуміли, функціональне програмування не має стану, але це не означає, що воно не може зберігати дані. Різниця полягає в тому, що якщо я напишу (Haskell) висловлювання у формі
let x = func value 3.14 20 "random"
in ...
Мені гарантовано, що значення x
завжди однаково ...
: ніщо не може змінити його. Подібним чином, якщо у мене є функція f :: String -> Integer
(функція, яка приймає рядок і повертає ціле число), я можу бути впевнений, що f
вона не змінить свій аргумент, не змінить жодних глобальних змінних, не запише дані у файл тощо. Як сказав sepp2k у коментарі вище, ця незмінність дійсно корисна для міркувань щодо програм: ви пишете функції, які згортають, обертають та калічать ваші дані, повертаючи нові копії, щоб ви могли зв'язати їх ланцюгом, і можете бути впевнені, що жодна з цих викликів функцій може зробити щось "шкідливе". Ви знаєте, що x
це завжди x
, і вам не доведеться хвилюватися, що хтось писав x := foo bar
десь між декларацієюx
та його використання, бо це неможливо.
Що робити, якщо я хочу прочитати введення від користувача? Як сказав KennyTM, ідея полягає в тому, що нечиста функція - це чиста функція, яка передає весь світ як аргумент і повертає як її результат, так і світ. Звичайно, ви не хочете насправді робити це: з одного боку, це жахливо незграбно, а з іншого, що станеться, якщо я повторно використаю той самий світовий об'єкт? Тож це якось абстрагується. Haskell обробляє це з типом IO:
main :: IO ()
main = do str <- getLine
let no = fst . head $ reads str :: Integer
...
Це говорить нам, що main
це дія введення-виведення, яка нічого не повертає; виконання цієї дії означає, що означає запуск програми Haskell. Правило полягає в тому, що типи IO ніколи не можуть уникнути дії IO; у цьому контексті ми вводимо цю дію за допомогою do
. Таким чином, getLine
повертає an IO String
, про що можна думати двома способами: по-перше, як дію, яка під час запуску створює рядок; по-друге, як рядок, який "заплямований" IO, оскільки отриманий нечисто. Перше правильніше, але друге може бути більш корисним. Копія <-
виймає String
з IO String
і зберігає в ньому, str
- але оскільки ми вже виконуємо операцію введення-виведення, нам доведеться обернути її назад, щоб вона не могла "втекти". Наступний рядок намагається прочитати ціле число ( reads
) і захоплює перший успішний збіг (fst . head
); це все чисто (без введення-виведення), тому ми даємо йому назву з let no = ...
. Потім ми можемо використовувати як no
і str
в ...
. Таким чином, ми зберігаємо нечисті дані (від getLine
до str
) та чисті дані ( let no = ...
).
Цей механізм роботи з IO є дуже потужним: він дозволяє вам відокремити чисту, алгоритмічну частину вашої програми від нечистої сторони, з боку взаємодії з користувачем, і забезпечити це на рівні типу. Ваша minimumSpanningTree
функція не може щось інше змінити у коді, написати повідомлення користувачеві тощо. Це безпечно.
Це все, що вам потрібно знати, щоб використовувати IO у Haskell; якщо це все, що ти хочеш, ти можеш зупинитися на цьому. Але якщо ви хочете зрозуміти, чому це працює, продовжуйте читати. (І зауважте, що ці матеріали стосуватимуться Haskell - інші мови можуть вибрати іншу реалізацію.)
Тож це, мабуть, здавалося трохи обманом, якимось чином додаючи домішок до чистого Хаскелла. Але це не так - виявляється, ми можемо реалізувати тип вводу-виводу цілком у чистому Haskell (якщо нам це дано RealWorld
). Ідея така: дія вводу-виводу IO type
- це те саме, що функція RealWorld -> (type, RealWorld)
, яка приймає реальний світ і повертає як об’єкт типу, так type
і модифікований RealWorld
. Потім ми визначаємо пару функцій, щоб ми могли використовувати цей тип, не сходячи з розуму:
return :: a -> IO a
return a = \rw -> (a,rw)
(>>=) :: IO a -> (a -> IO b) -> IO b
ioa >>= fn = \rw -> let (a,rw') = ioa rw in fn a rw'
Перший дозволяє нам говорити про дії вводу-виводу, які нічого не роблять: return 3
це дія введення-виведення, яка не запитує реальний світ, а просто повертається 3
. >>=
Оператор, оголошений «прив'язувати», дозволяють запускати дії введення - виведення. Він витягує значення з дії вводу-виводу, передає його та реальний світ через функцію і повертає результуючу дію вводу-виводу. Зверніть увагу, що >>=
виконується наше правило, згідно з яким ніколи не дозволяється отримувати результати результатів введення-виведення.
Потім ми можемо перетворити вищезазначене main
в наступний звичайний набір функціональних додатків:
main = getLine >>= \str -> let no = (fst . head $ reads str :: Integer) in ...
Перехід до виконання Haskell починається main
з початкового RealWorld
, і ми готові! Все чисто, воно просто має вигадливий синтаксис.
[ Редагувати: Як зазначає @Conal , насправді це не те, що використовує Haskell для здійснення вводу- виводу. Ця модель руйнується, якщо ви додаєте паралельність або взагалі будь-який спосіб, щоб світ змінився в середині дії вводу-виводу, тому для Хаскелла було б неможливо використовувати цю модель. Він точний лише для послідовних обчислень. Таким чином, може бути, що IO Хаскелла - трохи ухилення; навіть якщо це не так, це, звичайно, не зовсім це елегантно. За спостереженнями @ Conal, подивіться, що говорить Саймон Пейтон-Джонс у " Розв'язанні незручного загону" [pdf] , розділ 3.1; він представляє те, що могло б означати альтернативну модель за цими напрямками, але потім відмовляється від неї через її складність і бере інший варіант.]
Знову ж таки, це пояснює (в значній мірі), як введення-виведення та змінність загалом працює в Haskell; якщо це все, що ви хочете знати, ви можете припинити читати тут. Якщо ви хочете останню дозу теорії, продовжуйте читати, але пам’ятайте, що на даний момент ми відійшли від вашого питання дуже далеко!
Отож останнє: виявляється, ця структура - параметричний тип з return
і >>=
- дуже загальна; це називається монада, і do
позначення return
, і >>=
робота з будь-якою з них. Як ви побачили тут, монади не є чарівними; все, що є магічним, це те, що do
блоки перетворюються на виклики функцій. RealWorld
Типом є єдиним місцем , ми бачимо диво. Такі типи, як []
конструктор списку, також є монадами, і вони не мають нічого спільного з нечистим кодом.
Тепер ви знаєте (майже) все про поняття монади (крім кількох законів, які повинні бути виконані, та формального математичного визначення), але вам бракує інтуїції. В Інтернеті існує безглузда кількість підручників з монади; Мені подобається цей , але у вас є варіанти. Однак це, мабуть, вам не допоможе ; єдиний реальний спосіб отримати інтуїцію - це поєднання їх використання та читання декількох підручників у потрібний час.
Однак вам не потрібна ця інтуїція, щоб зрозуміти IO . Розуміння монад у цілому загалом - це вишенька, але ви можете використовувати IO прямо зараз. Ви можете використовувати його після того, як я показав вам першу main
функцію. Ви навіть можете поводитися з кодом введення-виведення так, ніби він написаний нечистою мовою! Але пам’ятайте, що існує основне функціональне уявлення: ніхто не обманює.
(PS: Вибачте за довжину. Я пішов трохи далеко).