Если языки функционального программирования не могут сохранять какое-либо состояние, как они делают некоторые простые вещи, такие как чтение ввода от пользователя (я имею в виду, как они его «хранят») или сохранение каких-либо данных в этом отношении?
Как вы уже поняли, функциональное программирование не имеет состояния, но это не значит, что оно не может хранить данные. Разница в том, что если я напишу оператор (Haskell) в строках
let x = func value 3.14 20 "random"
in ...
Я уверен, что значение x
всегда одно и то же ...
: ничто не может его изменить. Точно так же, если у меня есть функция f :: String -> Integer
(функция, принимающая строку и возвращающая целое число), я могу быть уверен, что f
она не изменит свой аргумент, не изменит какие-либо глобальные переменные, не запишет данные в файл и так далее. Как сказал sepp2k в комментарии выше, эта неизменяемость действительно полезна для рассуждений о программах: вы пишете функции, которые сворачивают, раскручивают и искажают ваши данные, возвращая новые копии, чтобы вы могли связать их вместе, и вы можете быть уверены, что ни один вызовы этих функций могут сделать что-нибудь «вредное». Вы знаете, что x
это всегда так x
, и вам не нужно беспокоиться о том, что кто-то написал x := foo bar
где-то между объявлениемx
и его использование, потому что это невозможно.
А что, если я хочу прочитать ввод от пользователя? Как сказал Кенни, идея состоит в том, что нечистая функция - это чистая функция, которая передает весь мир в качестве аргумента и возвращает как свой результат, так и мир. Конечно, на самом деле вы не хотите этого делать: с одной стороны, это ужасно неуклюже, а с другой - что произойдет, если я повторно использую один и тот же объект мира? Так что это как-то абстрагируется. Haskell обрабатывает это с помощью типа ввода-вывода:
main :: IO ()
main = do str <- getLine
let no = fst . head $ reads str :: Integer
...
Это говорит нам, что main
это действие ввода-вывода, которое ничего не возвращает; выполнение этого действия - это то, что означает запуск программы на Haskell. Правило состоит в том, что типы ввода-вывода никогда не могут избежать действия ввода-вывода; в этом контексте мы представляем это действие, используя do
. Таким образом, getLine
возвращает объект IO String
, который можно рассматривать двумя способами: во-первых, как действие, которое при запуске создает строку; во-вторых, как строка, "испорченная" ИО, поскольку она была получена нечисто. Первый более правильный, но второй может оказаться более полезным. <-
Берет String
из ряда IO String
и сохраняет его в str
бут , так как мы находимся в действии IO, мы должны обернуть его резервную копию, поэтому он не может «бежать». Следующая строка пытается прочитать целое число (reads
) и захватывает первое успешное совпадение (fst . head
); это все чисто (без ввода-вывода), поэтому мы даем ему имя с помощью ).let no = ...
. Затем мы можем использовать оба no
и str
в ...
. Таким образом, мы сохранили нечистые данные (из getLine
в str
) и чистые данные (let no = ...
Этот механизм для работы с вводом-выводом очень мощный: он позволяет вам отделить чистую, алгоритмическую часть вашей программы от нечистой стороны взаимодействия с пользователем и обеспечить это на уровне типа. Ваша minimumSpanningTree
функция не может изменить что-то еще в вашем коде, или написать сообщение вашему пользователю, и так далее. Это безопасно.
Это все, что вам нужно знать для использования ввода-вывода в Haskell; если это все, что ты хочешь, можешь остановиться здесь. Но если вы хотите понять, почему это работает, продолжайте читать. (И обратите внимание, что это будет специфично для 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
, и все готово! Все чисто, только с красивым синтаксисом.
[ Edit: Как отмечает @Conal , это не совсем то, что Haskell использует для ввода-вывода. Эта модель ломается, если вы добавляете параллелизм или какой-либо другой способ изменения мира в середине действия ввода-вывода, поэтому для Haskell было бы невозможно использовать эту модель. Это верно только для последовательных вычислений. Таким образом, может оказаться, что IO в Haskell - своего рода уловка; даже если это не так, это определенно не так элегантно. Замечание Пер @ Конала, см., Что говорит Саймон Пейтон-Джонс в Tackling the Awkward Squad [pdf] , раздел 3.1; он представляет то, что могло бы составить альтернативную модель в этом направлении, но затем отбрасывает ее из-за ее сложности и принимает другой курс.]
Опять же, это объясняет (в значительной степени), как IO и изменчивость в целом работают в Haskell; если это все, что вы хотите знать, можете перестать читать здесь. Если вам нужна последняя порция теории, продолжайте читать - но помните, что на данный момент мы очень далеко ушли от вашего вопроса!
И последнее: оказывается, что эта структура - параметрический тип с return
и >>=
- очень общая; это называется монада и do
нотация return
, и >>=
работать с любым из них. Как вы здесь видели, монады не волшебны; волшебство состоит в том, что do
блоки превращаются в вызовы функций. RealWorld
Типом является единственным местом , мы видим волшебство. Такие типы, как []
конструктор списка, также являются монадами и не имеют ничего общего с нечистым кодом.
Теперь вы знаете (почти) все о концепции монады (кроме нескольких законов, которые должны быть выполнены, и формального математического определения), но вам не хватает интуиции. В сети существует невероятное количество руководств по монадам; Мне нравится этот , но у вас есть варианты. Однако это, вероятно, вам не поможет ; Единственный реальный способ получить интуицию - это их сочетание и чтение пары руководств в нужное время.
Однако для понимания ввода-вывода эта интуиция не нужна . Общее понимание монад - это вишенка на торте, но вы можете использовать IO прямо сейчас. Вы сможете использовать его после того, как я покажу вам первую main
функцию. Вы даже можете рассматривать код ввода-вывода, как если бы он был написан на нечистом языке! Но помните, что есть основное функциональное представление: никто не обманывает.
(PS: извините за длину. Я зашел немного дальше.)