Ленивый hGetContents. Баг или фича? (Haskell)

в 21:17, , рубрики: haskell, ленивые вычисления, метки:

Меня давно беспокоит одна тема. Вот решил высказаться и услышать, что думают люди по этому поводу. Речь пойдет о функции hGetContents. Если вы когда-нибудь работали с файлами, то вы знаете, что эта функция возвращает содержимое файла (потока). Вот типичный пример использования этой функции.

import System.IO

main = do 
	file <- openFile "1.txt" ReadMode
	content <- hGetContents file
	print content
	hClose file
-- результат: выводит содержимое файла на экран


Все очень банально — открываем файл, считываем содержимое, выводим на экран, закрываем файл. Фича фунции hGetContents (а точнее фича Haskell-я) в том, что она ленивая. А именно, данные из файла считаются не сразу целиком, а будут считываться по мере необходимости. Например, вот такая программа

import System.IO

main = do 
	file <- openFile "1.txt" ReadMode
	content <- hGetContents file
	print $ take 3 content  
	hClose file
-- результат: выводит первые 3 символа из файла на экран

считает из файла только первые 3 символа.
Что тут можно сказать, отличная фича! Это очень удобно, оперировать переменной content (содержимое файла), но при этом знать, что Haskell возмет из него только то, что нужно, и ничего лишнего.
А теперь рассмотрим следующий пример

import System.IO

main = do 
	file <- openFile "1.txt" ReadMode
	content <- hGetContents file
	hClose file
	print content
-- результат: выводит пустую строку

Здесь мы просто переставил местами две последние строчки. Результатом будет выведенная на экран пустая строка. Почему так получилось? Причина в том, что мы пытаемся обратиться к содержимому файла после того, как файл уже закрыт. При вызове hGetContents данные никуда не считывались, просто в переменной content сохранилась некоторая ссылка на этот файл. А потом, когда нам понадобилось содержимое content, оказалось, что файл уже закрыт.
Ну и что такого, подумаете Вы, просто нужно использовать переменную content до закрытия файла.

Я долго думал над этим примером и чувство «что-то здесь не так» не оставляло меня в покое. Разве это не нарушает чистоту Haskell-я. Я же написал корректную по сути программу, а она выдает не правильный результат. Я что должен думать о том, в какой момент он обратится к этой переменной и что он там вообще делает под капотом? Это уже не Haskell, это, извините за выражение, C++ какой-то.
В итоге я пришел к выводу, что эта «фича» — не фича, а баг. Если я вас не убедил, то вот несколько доводов:

  • 1. В лямбда-исчислении существуют различные стратегии редукции лямбда терма: полная бета-редукция, нормальный порядок вычислений, вызов по имени (в haskell используется оптимизированный вариант вызов по необходимости), вызов по значению(энергичные вычисления). Существует теорема, которая гласит, что любой лямбда-терм независимо от стратегии вычислений редуцируется к одной и той же нормальной форме, если она вообще существует. Т.е. при любом порядке вычислений (ленивом или энергичном) должен получаться один и тот же результат. Правда, существуют примеры лямбда-термов, вычисление которых зацикливается при энергичной стратегии, но не зацикливается при ленивой (например работа с бесконечными списками). Но если оба вычисления завершаются, то они должны давать одинаковый результат!
    (Кстати, обратного примера, когда ленивый вариант зациклился, а энергичный — нет, не существует. В этом смысле ленивая стратегия самая «аккуратная»)
    Если мы рассмотрим наш последний пример и представим, что Haskell вычисляет его энергично, то получится, что наша программа должна работать корректно и выдавать содержимое файла. Т.е. в энергичном режиме один результат, а в ленивом — другой. Вы можете возразить, что программа — это не лямбда-терм и не чистая функция. В каком-то смысле это верно. Но давайте вспомним, для чего были придуманы монады. Разве не для того, чтобы представить программу, как некоторую чистую функцию, которая берет на вход состояние внешнего мира, и выдает на выход новое состояние?
  • 2. Все-таки почему так получается? В Haskell все функции чистые. Результат вычисления зависит только от переданных аргументов. Это значит, что мы можем отложить вычисление «на потом», и результат от этого не изменится. В случае, когда мы вызываем hGetContents, результат функции зависит не только от аргумента, но и от состояния системы в данный момент. Имеем ли мы право откладывать «на потом» вычисление, которое зависит от состояния системы? В идеале программа должна работать так: открываем файл, закрываем файл, при вызове print возвращаемся в прошлое (когда файл был еще открыт), читаем его содержимое, возвращаемся в будущее, выводим на экран.

Проблема тут еще в том, что мы никак не можем «заставить» Haskell вычислить переменную content до закрытия файла (если кто-то знает способ, напишите).

К чему я все это? Вы только не поймите меня не правильно. Я очень люблю Haskell. За его чистоту, ленивость, функциональность и еще много чего, что не выразить словами. Просто я вдруг узнал, что он не такой «чистый», как я раньше думал. Осталось какое-то смешанное чувство, что вроде бы и фича-то хорошая, но как-то «грязьненько» от неё стало.
Возможно у меня просто паранойя. Хотелось бы услышать ваше мнение, баг или фича?

Автор: Tazman

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js