Как лень и ввод-вывод работают вместе в Haskell?

Я пытаюсь получить более глубокое понимание лени в Haskell.

Сегодня я представил себе следующий фрагмент:

data Image = Image { name :: String, pixels :: String }

image :: String -> IO Image
image path = Image path <$> readFile path

Апелляция здесь заключается в том, что я могу просто создать экземпляр Image и передать его; если мне нужны данные изображения, он будет читаться лениво - если нет, можно избежать затрат времени и памяти на чтение файла:

 main = do
   image <- image "file"
   putStrLn $ length $ pixels image

Но как это работает? Как лень совместима с IO? Будет ли readFile вызываться независимо от того, получаю ли я доступ к pixels image или не удастся ли выполнить runtime, если он никогда не ссылается на него?

Если изображение действительно читается лениво, то не возможно ли, что действия ввода-вывода могут не совпадать с порядком? Например, что, если сразу после вызова image я удаляю файл? Теперь вызов putStrLn ничего не найдет, когда он попытается прочитать.

Ответ 1

Как laziness совместим с I/O?

Короткий ответ: Это не так.


Длинный ответ: IO действия строго упорядочены, в основном из-за причин, о которых вы думаете. Разумеется, любые чистые вычисления, выполненные с результатами, могут быть ленивыми; например, если вы читаете в файле, выполняете некоторую обработку и затем распечатываете некоторые результаты, вероятно, что обработка, не требуемая для вывода, не будет оцениваться. Тем не менее, весь файл будет считан, даже части, которые вы никогда не используете. Если вы хотите ленивый ввод-вывод, у вас есть примерно два варианта:

  • Сверните свои собственные явные процедуры ленивой загрузки и такие, как на любом строгом языке. Кажется раздражающим, предоставленным, но, с другой стороны, Хаскелл делает строгий, императивный язык. Если вы хотите попробовать что-то новое и интересное, попробуйте посмотреть Iteratees.

  • Чит, как мошенник. Функции такие как hGetContents будут делать ленивые по требованию ввода-вывода для вас, без вопросов. Какой улов? Он (технически) нарушает ссылочную прозрачность. Чистый код может косвенно вызвать побочные эффекты, и могут произойти смешные вещи, связанные с упорядочением побочных эффектов, если ваш код действительно запутан. hGetContents и друзья реализованы с помощью unsafeInterleaveIO, что... точно то, что он говорит на жестяне. Это нигде почти не может взорваться в вашем лице, используя unsafePerformIO, но считайте, что вас предупредили.

Ответ 2

Lazy I/O нарушает чистоту Haskell. Результаты readFile действительно производятся лениво, по требованию. Порядок, в котором происходят действия ввода-вывода, не фиксирован, поэтому да, они могут произойти "не в порядке". Проблема удаления файла перед вытягиванием пикселей реальна. Короче говоря, ленивый ввод-вывод - отличное удобство, но это инструмент с очень острыми краями.

В книге о реальном мире Haskell есть длительное обращение с ленивыми вводами-выводами и проходит некоторые из подводных камней.