Обычно я слышал, что производственный код не должен использовать ленивый ввод-вывод. У меня вопрос, почему? Можно ли когда-нибудь использовать ленивый ввод-вывод вне игры? И что делает альтернативы (например, счетчики) лучше?
Обычно я слышал, что производственный код не должен использовать ленивый ввод-вывод. У меня вопрос, почему? Можно ли когда-нибудь использовать ленивый ввод-вывод вне игры? И что делает альтернативы (например, счетчики) лучше?
Ответы:
У ленивого ввода-вывода есть проблема, заключающаяся в том, что высвобождение любого полученного вами ресурса в некоторой степени непредсказуемо, так как это зависит от того, как ваша программа потребляет данные - от ее «модели спроса». Как только ваша программа удалит последнюю ссылку на ресурс, сборщик мусора в конечном итоге запустит и освободит этот ресурс.
Ленивые потоки - очень удобный стиль для программирования. Вот почему оболочки-оболочки так интересны и популярны.
Однако, если ресурсы ограничены (как в сценариях с высокой производительностью или в производственных средах, которые предполагают масштабирование до пределов машины), использование GC для очистки может быть недостаточной гарантией.
Иногда вам нужно быстро высвободить ресурсы, чтобы улучшить масштабируемость.
Так каковы же альтернативы ленивому вводу-выводу, которые не означают отказа от инкрементной обработки (которая, в свою очередь, потребляет слишком много ресурсов)? Итак, у нас есть foldl
обработка на основе, также известная как итераторы или счетчики, введенные Олегом Киселевым в конце 2000-х годов и с тех пор популяризированные рядом сетевых проектов.
Вместо того, чтобы обрабатывать данные как ленивые потоки или в одном большом пакете, мы вместо этого абстрагируемся от строгой обработки на основе фрагментов с гарантированным завершением ресурса после прочтения последнего фрагмента. Это суть программирования на основе итераций, которое предлагает очень хорошие ограничения ресурсов.
Обратной стороной IO на основе итераций является то, что он имеет несколько неудобную модель программирования (примерно аналогичную программированию на основе событий, а не хорошему управлению на основе потоков). Это определенно продвинутая техника для любого языка программирования. И для подавляющего большинства проблем программирования ленивый ввод-вывод вполне удовлетворителен. Однако, если вы будете открывать много файлов или разговаривать по множеству сокетов, или иным образом использовать множество одновременных ресурсов, подход итеративного (или перечислителя) может иметь смысл.
Донс дал очень хороший ответ, но он упустил из виду то, что (для меня) является одной из самых убедительных особенностей итераций: они упрощают рассуждение об управлении пространством, поскольку старые данные должны явно сохраняться. Рассмотреть возможность:
average :: [Float] -> Float
average xs = sum xs / length xs
Это хорошо известная утечка места, потому что весь список xs
должен храниться в памяти для вычисления обоих sum
и length
. Сделать эффективного потребителя можно, создав складку:
average2 :: [Float] -> Float
average2 xs = uncurry (/) <$> foldl (\(sumT, n) x -> (sumT+x, n+1)) (0,0) xs
-- N.B. this will build up thunks as written, use a strict pair and foldl'
Но делать это для каждого потокового процессора несколько неудобно. Есть некоторые обобщения ( Conal Elliott - Beautiful Fold Zipping ), но они, похоже, не прижились. Однако итераторы могут дать вам аналогичный уровень выражения.
aveIter = uncurry (/) <$> I.zip I.sum I.length
Это не так эффективно, как сворачивание, потому что список по-прежнему повторяется несколько раз, однако он собирается кусками, так что старые данные могут быть эффективно собраны мусором. Чтобы нарушить это свойство, необходимо явно сохранить весь ввод, например, с stream2list:
badAveIter = (\xs -> sum xs / length xs) <$> I.stream2list
Работа над итерациями как моделью программирования еще не завершена, но сейчас она намного лучше, чем год назад. Мы учимся , что комбинаторы полезны (например zip
, breakE
, enumWith
) , и которые в меньшей степени, в результате чего встроенный iteratees и комбинаторы обеспечивают постоянно больше выразительности.
Тем не менее, Донс прав в том, что это продвинутая техника; Я бы точно не стал использовать их для решения каждой проблемы ввода-вывода.
Я все время использую ленивый ввод-вывод в производственном коде. Как сказал Дон, это проблема только при определенных обстоятельствах. Но для чтения нескольких файлов он отлично работает.
Обновление: недавно в haskell-cafe Олег Киселёв показал, что unsafeInterleaveST
(который используется для реализации ленивого ввода-вывода в монаде ST) очень небезопасен - он нарушает эквациональные рассуждения. Он показывает, что это позволяет построить bad_ctx :: ((Bool,Bool) -> Bool) -> Bool
такое, что
> bad_ctx (\(x,y) -> x == y)
True
> bad_ctx (\(x,y) -> y == x)
False
хотя ==
и коммутативен.
Еще одна проблема с отложенным вводом-выводом: фактическую операцию ввода-вывода можно отложить до тех пор, пока не станет слишком поздно, например, после закрытия файла. Цитата из Haskell Wiki - Проблемы с ленивым вводом-выводом :
Например, распространенная ошибка новичков - закрыть файл до того, как его прочитали:
wrong = do fileData <- withFile "test.txt" ReadMode hGetContents putStr fileData
Проблема в том, что withFile закрывает дескриптор до того, как fileData принудительно. Правильный способ - передать весь код в withFile:
right = withFile "test.txt" ReadMode $ \handle -> do fileData <- hGetContents handle putStr fileData
Здесь данные потребляются до завершения работы withFile.
Это часто неожиданная ошибка, которую легко исправить.
См. Также: Три примера проблем с отложенным вводом-выводом .
hGetContents
и withFile
бессмысленно, потому что первое переводит дескриптор в «псевдозакрытое» состояние и будет обрабатывать закрытие за вас (лениво), поэтому код в точности эквивалентен readFile
или даже openFile
без него hClose
. Это в основном то , что ленив I / O является . Если вы не используете readFile
, getContents
или hGetContents
вы не используете ленивое I / O. Например line <- withFile "test.txt" ReadMode hGetLine
отлично работает.
hGetContents
будет обрабатывать закрытие файла за вас, также допустимо закрывать его самостоятельно «раньше» и помогает обеспечить предсказуемое высвобождение ресурсов.
Еще одна проблема с ленивым вводом-выводом, о которой до сих пор не упоминалось, заключается в ее удивительном поведении. В обычной программе на Haskell иногда бывает трудно предсказать, когда будет оцениваться каждая часть вашей программы, но, к счастью, из-за чистоты это действительно не имеет значения, если у вас нет проблем с производительностью. Когда вводится ленивый ввод-вывод, порядок оценки вашего кода фактически влияет на его значение, поэтому изменения, которые вы привыкли считать безобидными, могут вызвать у вас настоящие проблемы.
В качестве примера, вот вопрос о коде, который выглядит разумным, но становится более запутанным из-за отложенного ввода-вывода: withFile vs. openFile
Эти проблемы не всегда фатальны, но об этом стоит подумать и о достаточно серьезной головной боли, поэтому я лично избегаю ленивого ввода-вывода, если нет реальной проблемы с выполнением всей работы заранее.