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