Що такого поганого в лінивому введенні / виведенні?


89

Я загалом чув, що у виробничому коді слід уникати використання лінивих входів / виходів. Моє питання: чому? Чи коли-небудь нормально використовувати лінивий ввід-вивід за межами просто іграшок? І що робить альтернативи (наприклад, перелічувачі) кращими?

Відповіді:


81

Ледачий введення-випуск має проблему з тим, що вивільнення будь-якого придбаного вами ресурсу є дещо непередбачуваним, оскільки це залежить від того, як ваша програма споживає дані - її "модель попиту". Як тільки ваша програма скине останнє посилання на ресурс, GC врешті-решт запустить і випустить цей ресурс.

Ледачі потоки - це дуже зручний стиль для програмування. Ось чому оболонки так веселі та популярні.

Однак, якщо ресурси обмежені (як у високоефективних сценаріях або виробничих середовищах, які очікують масштабування до межі машини), покладання на GC для очищення може бути недостатньою гарантією.

Іноді доводиться охоче звільняти ресурси, щоб покращити масштабованість.

То які альтернативи лінивому вводу-виводу не означають відмови від поступової обробки (що, у свою чергу, споживатиме занадто багато ресурсів)? Що ж, ми foldlбазуємо обробку даних, вона ж ітератори чи перелічувачі, запроваджену Олегом Кисельовим наприкінці 2000-х років , і з тих пір популяризована низкою мережевих проектів.

Замість того, щоб обробляти дані як ліниві потоки або в одній величезній партії, ми замість цього абстрагуємось над суворою обробкою на основі фрагментів, з гарантованою доопрацюванням ресурсу після прочитання останнього фрагмента. Це суть програмування на основі ітерацій, і таке, що пропонує дуже приємні обмеження ресурсів.

Недоліком введення-виводу на основі ітерацій є те, що він має дещо незграбну модель програмування (приблизно аналогічно програмуванню на основі подій проти гарного управління на основі потоків). Це, безумовно, вдосконалена техніка будь-якої мови програмування. І для переважної більшості проблем з програмуванням лінивий ВВ цілком задовільний. Однак, якщо ви будете відкривати багато файлів, розмовляти на багатьох сокетах, або по-іншому використовувати багато одночасних ресурсів, підхід ітератора (або перелічувача) може мати сенс.


22
Оскільки я щойно перейшов за посиланням на це старе питання з обговорення ледачого вводу-виводу, я подумав додати примітку, що з тих пір більша частина незграбності ітерацій була витіснена новими потоковими бібліотеками, такими як труби та трубопроводи .
Ørjan Johansen

40

Донс дав дуже добру відповідь, але він пропустив, що є (для мене) однією з найпереконливіших особливостей ітераторів: вони полегшують міркування щодо управління космосом, оскільки старі дані повинні бути явно збережені. Розглянемо:

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 і комбінатори забезпечують постійно більше виразності.

Тим не менш, Донс правильно, що вони є передовою технікою; Я б, звичайно, не використовував їх для кожної проблеми вводу-виводу.


25

Я постійно використовую ліниві операції вводу-виводу у виробничому коді. Це проблема лише за певних обставин, як згадував Дон. Але для простого читання декількох файлів це чудово працює.


Я також використовую ліниві введення-виведення. Я звертаюся до ітераторів, коли хочу більше контролю над управлінням ресурсами.
Джон Л,

20

Оновлення: Нещодавно в 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

Проблема полягає у тому, що File закриває дескриптор до примусового використання fileData. Правильний спосіб - передати весь код в withFile:

right = withFile "test.txt" ReadMode $ \handle -> do
    fileData <- hGetContents handle
    putStr fileData

Тут дані споживаються перед закінченням з файлом.

Це часто є несподіваною та простою у виконанні помилкою.


Див. Також: Три приклади проблем із лінивим введенням / виведенням .


Насправді комбінувати hGetContentsі withFileбезглуздо, оскільки перший ставить дескриптор у "псевдозакритий" стан і буде обробляти закриття для вас (ліниво), тому код точно еквівалентний readFileабо навіть openFileбез нього hClose. Це в основному те , що ледачий I / O є . Якщо ви не використовуєте readFile, getContentsабо hGetContentsви не використовуєте ліниве I / O. Наприклад, line <- withFile "test.txt" ReadMode hGetLineчудово працює.
Dag

1
@Dag: хоча сам вирішить питання hGetContentsзакриття файлу для вас, також допустимо закрити його самостійно "достроково" і допомагає забезпечити передбачуваний вихід ресурсів.
Бен Міллвуд,

17

Ще однією проблемою ледачого введення-виведення, про яку досі не згадували, є те, що вона має дивовижну поведінку. У звичайній програмі Haskell іноді буває важко передбачити, коли оцінюється кожна частина вашої програми, але, на щастя, завдяки чистоті це насправді не має значення, якщо у вас немає проблем із продуктивністю. Коли вводиться ледачий введення-виведення, порядок оцінки вашого коду насправді впливає на його значення, тому зміни, про які ви звикли вважати нешкідливими, можуть викликати у вас справжні проблеми.

Як приклад, ось питання про код, який виглядає обгрунтовано, але робить його більш заплутаним через відкладене введення-виведення: withFile проти openFile

Ці проблеми не завжди фатальні, але інша річ, про яку слід подумати, і досить сильний головний біль, про який я особисто уникаю ледачого введення-виведення, якщо немає реальних проблем із виконанням усієї роботи заздалегідь.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.