Меня давно беспокоит одна тема. Вот решил высказаться и услышать, что думают люди по этому поводу. Речь пойдет о функции hGetContents. Если вы когда-нибудь работали с файлами, то вы знаете, что эта функция возвращает содержимое файла (потока). Вот типичный пример использования этой функции.
Все очень банально — открываем файл, считываем содержимое, выводим на экран, закрываем файл. Фича фунции hGetContents (а точнее фича Haskell-я) в том, что она ленивая. А именно, данные из файла считаются не сразу целиком, а будут считываться по мере необходимости. Например, вот такая программа
считает из файла только первые 3 символа.
Что тут можно сказать, отличная фича! Это очень удобно, оперировать переменной content (содержимое файла), но при этом знать, что Haskell возмет из него только то, что нужно, и ничего лишнего.
А теперь рассмотрим следующий пример
Здесь мы просто переставил местами две последние строчки. Результатом будет выведенная на экран пустая строка. Почему так получилось? Причина в том, что мы пытаемся обратиться к содержимому файла после того, как файл уже закрыт. При вызове hGetContents данные никуда не считывались, просто в переменной content сохранилась некоторая ссылка на этот файл. А потом, когда нам понадобилось содержимое content, оказалось, что файл уже закрыт.
Ну и что такого, подумаете Вы, просто нужно использовать переменную content до закрытия файла.
Проблема тут еще в том, что мы никак не можем «заставить» Haskell вычислить переменную content до закрытия файла (если кто-то знает способ, напишите). Например, нам нужно написать функцию, которая открывает файл, читает содержимое, парсит некоторую структуру, закрывает файл, выдает полученную структуру как результат.
Можно, конечно, всю программу поместить между openFile и hClose, а что делать, если нужно считать содержимое из файла, подкорректировать и записать в тот же файл?
Я долго думал над этим примером и чувство «что-то здесь не так» не оставляло меня в покое. Разве это не нарушает чистоту Haskell-я. Я же написал корректную по сути программу, а она выдает не правильный результат. Я что должен думать о том, в какой момент он обратится к этой переменной и что он там вообще делает под капотом? Это уже не Haskell, это, извините за выражение, C++ какой-то.
В итоге я пришел к выводу, что эта «фича» — не фича, а баг. Если я вас не убедил, то вот несколько доводов:
К чему я все это? Вы только не поймите меня не правильно. Я очень люблю Haskell. За его чистоту, ленивость, функциональность и еще много чего, что не выразить словами. Просто я вдруг узнал, что он не такой «чистый», как я раньше думал. Осталось какое-то смешанное чувство, что вроде бы и фича-то хорошая, но как-то «грязьненько» от неё стало.
Возможно у меня просто паранойя. Хотелось бы услышать ваше мнение, баг или фича?
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 вычислить переменную content до закрытия файла (если кто-то знает способ, напишите). Например, нам нужно написать функцию, которая открывает файл, читает содержимое, парсит некоторую структуру, закрывает файл, выдает полученную структуру как результат.
Вроде бы элементарный пример, но он НЕ РАБОТАЕТ. По той же причине. То, что мы передали содержимое файла в некоторую функцию read, ничего не изменило, потому что она тоже отложилась «на потом». И что бы мы не сделали с content, это не заставит Haskell считать содержимое из файла (если мы, конечно, не выведем его на экран).{-# OPTIONS_GHC -XScopedTypeVariables #-} import System.IO parseFile :: (Read a) => String -> IO a parseFile fileName = do file <- openFile fileName ReadMode content <- hGetContents file rezult <- return $ read content hClose file return rezult main = do a :: Int <- parseFile "1.txt" print a --результат: ошибка
Можно, конечно, всю программу поместить между openFile и hClose, а что делать, если нужно считать содержимое из файла, подкорректировать и записать в тот же файл?
Я долго думал над этим примером и чувство «что-то здесь не так» не оставляло меня в покое. Разве это не нарушает чистоту Haskell-я. Я же написал корректную по сути программу, а она выдает не правильный результат. Я что должен думать о том, в какой момент он обратится к этой переменной и что он там вообще делает под капотом? Это уже не Haskell, это, извините за выражение, C++ какой-то.
В итоге я пришел к выводу, что эта «фича» — не фича, а баг. Если я вас не убедил, то вот несколько доводов:
- 1. В лямбда-исчислении существуют различные стратегии редукции лямбда терма: полная бета-редукция, нормальный порядок вычислений, вызов по имени (в haskell используется оптимизированный вариант вызов по необходимости), вызов по значению(энергичные вычисления). Существует теорема, которая гласит, что любой лямбда-терм независимо от стратегии вычислений редуцируется к одной и той же нормальной форме, если она вообще существует. Т.е. при любом порядке вычислений (ленивом или энергичном) должен получаться один и тот же результат. Правда, существуют примеры лямбда-термов, вычисление которых зацикливается при энергичной стратегии, но не зацикливается при ленивой (например работа с бесконечными списками). Но если оба вычисления завершаются, то они должны давать одинаковый результат!
(Кстати, обратного примера, когда ленивый вариант зациклился, а энергичный — нет, не существует. В этом смысле ленивая стратегия самая «аккуратная»)
Если мы рассмотрим наш последний пример и представим, что Haskell вычисляет его энергично, то получится, что наша программа должна работать корректно и выдавать содержимое файла. Т.е. в энергичном режиме один результат, а в ленивом — другой. Вы можете возразить, что программа — это не лямбда-терм и не чистая функция. В каком-то смысле это верно. Но давайте вспомним, для чего были придуманы монады. Разве не для того, чтобы представить программу, как некоторую чистую функцию, которая берет на вход состояние внешнего мира, и выдает на выход новое состояние?
- 2. Все-таки почему так получается? В Haskell все функции чистые. Результат вычисления зависит только от переданных аргументов. Это значит, что мы можем отложить вычисление «на потом», и результат от этого не изменится. В случае, когда мы вызываем hGetContents, результат функции зависит не только от аргумента, но и от состояния системы в данный момент. Имеем ли мы право откладывать «на потом» вычисление, которое зависит от состояния системы? В идеале программа должна работать так: открываем файл, закрываем файл, при вызове print возвращаемся в прошлое (когда файл был еще открыт), читаем его содержимое, возвращаемся в будущее, выводим на экран.
К чему я все это? Вы только не поймите меня не правильно. Я очень люблю Haskell. За его чистоту, ленивость, функциональность и еще много чего, что не выразить словами. Просто я вдруг узнал, что он не такой «чистый», как я раньше думал. Осталось какое-то смешанное чувство, что вроде бы и фича-то хорошая, но как-то «грязьненько» от неё стало.
Возможно у меня просто паранойя. Хотелось бы услышать ваше мнение, баг или фича?