Я не уверен, что разработанная вами интуиция (как описано в написанном вами ответе на этот вопрос) настолько точна. Позвольте мне попытаться дать вам лучшую интуицию. Вы также можете найти этот мой старый ответ полезным, хотя вопрос там был совсем другим.
В Haskell значение типа IO a
(для любого типа a
так что IO Int
или IO String
или что-либо еще) иногда называют «действием ввода-вывода», но как @WillNess упоминается в комментарии, его лучше всего рассматривать как «рецепт ввода-вывода». Для этих рецептов мы рассматриваем «оценку» и «исполнение» как полностью отдельные операции. Оценка выражение типа IO a
похоже на запись рецепта. Результатом вычисления выражения типа IO Int
является значение типа IO Int
. Получение этого значения не выполняет ввод-вывод и не требует времени для разговора, даже если базовый ввод-вывод включает задержки или другие медленные операции. Это оцененное значение IO a
может быть передано, сохранено, продублировано, изменено, объединено с другими IO
рецептами или полностью проигнорировано, и все это без каких-либо фактических операций ввода-вывода.
В отличие от этого выполнение результирующего рецепта - это процесс фактического выполнения операций ввода-вывода. Результатом выполнения и IO Int
является Int
. Если для получения этого Int
потребуется 20 минут задержек, доступа к файлу и / или реквизиции почтового голубя, операция займет некоторое время. Если вы выполните один и тот же рецепт дважды, во второй раз он не будет go быстрее.
Почти весь код, который мы пишем в Haskell , оценивает рецепты ввода-вывода без их выполнения.
Когда выполняется код:
slowOne = do
threadDelay (10 ^ 6)
return 1
, он фактически просто оценивает (записывает) рецепт ввода-вывода. Оценка этого рецепта, очевидно, включает в себя оценку do-block. Это не делает I / O; он просто оценивает (записывает) каждый из рецептов в блоке do и объединяет их в больший письменный рецепт.
В частности, оценка slowOne
включает в себя:
Оценка рецепта threadDelay (10 ^ 6)
. Для этого нужно вычислить арифметическое c выражение 10 ^ 6
и вызвать для него функцию threadDelay
. Эта функция реализована (для не поточной среды выполнения) как:
threadDelay :: Int -> IO ()
threadDelay time = IO $ \s -> some_function_of_s
То есть она оборачивает функцию в конструкторе IO
для получения значения типа IO ()
. Критически, это фактически не задерживает поток. Он просто создает (обернутое) функциональное значение. Кстати, в конструкторе IO
нет ничего волшебного. Эта threadDelay
функция аналогична написанию одинаково немагического:
justAFunction :: Int -> Maybe (Int -> Int)
justAFunction c = Just (\x -> c*x)
Оценка рецепта return 1
. Это также просто создает значение, заключенное в конструктор IO
. В частности, это (обернутое и полностью немагическое) функциональное значение, которое выглядит примерно так:
IO (\s -> (s, 1))
Объединение этих двух оцененных рецептов последовательно в более длинный рецепт. Этот новый комбинированный рецепт представляет собой значение типа IO Int
, которое будет присвоено slowOne
.
Аналогично, когда вычисляется следующий код:
infiniteInts :: [IO Integer]
infiniteInts = loop slowOne
where
loop :: IO Integer -> [IO Integer]
loop ioInt = ioInt : loop (fmap (+1) ioInt)
вы не выполняете IO. Вы просто оцениваете рецепты ввода-вывода и структуры данных, которые содержат рецепты ввода-вывода. В частности, вы оцениваете это выражение как значение типа [IO Integer]
, состоящее из бесконечного списка IO Integer
значений / рецептов. Первый рецепт в списке - slowOne
. Второй рецепт в списке:
fmap (+1) slowOne
Это требует слова объяснения. Когда это выражение вычисляется, оно создает новый рецепт, который может быть написан с использованием эквивалентного блока do:
fmap_plus_one_of_slowOne = do
x <- slowOne
return (x + 1)
Учитывая, как определено slowOne
, это фактически эквивалентно автономному рецепту, который мы получить путем оценки:
fmap_plus_one_of_slowOne = do
threadDelay (10 ^ 6)
return 2
Аналогично, третий рецепт в списке:
fmap (+1) (fmap (+1) slowOne)
соответствует эквиваленту рецепта:
fmap_plus_one_of_fmap_plus_one_of_slowOne = do
threadDelay (10 ^ 6)
return 3
Теперь, последняя часть вашей программы:
mapM_
(\ioInt -> do
i <- ioInt
print i
)
infiniteInts
Вас может удивить, что, когда этот код оценивается, мы все еще только оцениваем , а не выполняем рецепты. Когда эта функция mapM_
оценена, она создает новый рецепт. Рецепт, который он создает, можно описать словами:
"Возьмите каждый рецепт в списке infiniteInts
. Извините за неправильный выбор имени - это не список целых чисел, но список рецептов IO для создания целых чисел. Хорошо, что вы компьютер и не будете смущены этим, а? В любом случае, возьмите каждый из этих рецептов в последовательности и передайте их этой функции У меня есть здесь, чтобы сгенерировать новый рецепт. Затем запустите список рецептов по порядку. Вы записываете это, верно? Стоп, пока не выполняйте ! Просто запишите его! "
Итак, подведем итог:
slowOne
это рецепт
do threadDelay (10 ^ 6)
return 1
fmap (+1) slowOne
is так же, как рецепт:
do threadDelay (10 ^ 6)
return 2
аналогично, fmap (+1) (fmap (+1) slowOne)
на самом деле просто рецепт
do threadDelay (10 ^ 6)
return 3
и так далее
поэтому infiniteInts
- это список рецептов:
infiniteInts =
[ do { threadDelay (10 ^ 6); return 1 }
, do { threadDelay (10 ^ 6); return 2 }
, do { threadDelay (10 ^ 6); return 3 }
, ... ]
Учитывая значение рецепта mapM_ ...
, если Haskell допускается бесконечно длинная программа мс, мы могли бы написать весь этот рецепт с нуля следующим образом:
do -- first recipe
threadDelay (10 ^ 6)
i <- return 1
print i
-- second recipe
threadDelay (10 ^ 6)
i <- return 2
print i
-- third recipe
threadDelay (10 ^ 6)
i <- return 3
print i
-- etc.
Это рецепт, который получается в результате вычисления выражения mapM_ ...
.
Наконец, мы получаем только часть вашей программы , которая выполняет рецепт ввода-вывода, а не просто оценивает его. Эта часть:
main = ...
Когда вы называете рецепт main
, вы указываете Haskell выполнить его при запуске программы. Как вы можете видеть из оцененного значения рецепта, который вы присвоили main
, это комбинированный рецепт, включающий рецепты с перемежением threadDelay
и print
, поэтому при выполнении он выводит растущий список целых чисел с задержками до каждое целое число.
Примечание о лени и строгости ... Лень не играет никакой роли в вышеописанном процессе (ну, за исключением того, что мы можем создать бесконечный список без блокировки машины). Когда я говорю «оценить» выше, не имеет значения, является ли оценка строгой и происходит ли она немедленно или технически откладывается до тех пор, пока она не понадобится. Точка, в которой необходимо , может быть , когда он выполняется, но оценка (запись рецепта) и выполнение (следование рецепту) по-прежнему являются разными процессами, даже если они происходят один за другим.