Ответ 1
Я думаю, что большая часть проблем с "утечками строгости" происходит потому, что у людей нет хорошей концептуальной модели. Haskellers без хорошей концептуальной модели имеют тенденцию иметь и распространять суеверие, которое более строгое. Возможно, эта интуиция проистекает из их результатов, связанных с малыми примерами и плотными циклами. Но это неверно. Столь же важно быть ленивым в нужное время, чтобы быть строгим в нужное время.
Существует два лагеря типов данных, которые обычно называются "данные" и "codata". Очень важно уважать шаблоны каждого из них.
- Операции, которые производят "данные" (Int, ByteString,...), должны быть принудительно близки к тому, где они происходят. Если я добавлю номер в аккумулятор, я стараюсь, чтобы он был вынужден, прежде чем добавить еще один. Здесь очень важно хорошее понимание лень, особенно его условный характер (т.е. Предложения строгости не принимают форму "
X
получает оценку", а скорее "когда оцениваетсяY
, так чтоX
" ). - Операции, которые производят и потребляют "codata" (списки большинства времени, деревья, большинство других рекурсивных типов), должны делать это постепенно. Обычно преобразование codata → codata должно давать некоторую информацию для каждого бита информации, которую они потребляют (по модулю пропускается, как
filter
). Еще одна важная часть для кодат - это то, что вы используете ее линейно, когда это возможно, т.е. Используйте хвост списка ровно один раз; используйте каждую ветвь дерева ровно один раз. Это гарантирует, что GC может собирать куски по мере их потребления.
Вещи занимают особую заботу, когда у вас есть кодаты, содержащие данные. Например. iterate (+1) 0 !! 1000
закончит тем, что произведет тон размером 1000, прежде чем оценивать его. Вам нужно снова подумать об условной строгости - способ предотвратить этот случай - обеспечить, чтобы при использовании минус списка был добавлен его элемент. iterate
нарушает это, поэтому нам нужна лучшая версия.
iterate' :: (a -> a) -> a -> [a]
iterate' f x = x : (x `seq` iterate' f (f x))
Когда вы начинаете сочинять вещи, конечно, становится труднее сказать, когда происходят плохие случаи. В целом сложно создавать эффективные структуры данных/функции, которые одинаково хорошо работают с данными и кодами, и важно помнить, что это (даже в полиморфной обстановке, где это не гарантировано, вам следует иметь в виду и попробовать уважать его).
Совместное использование является сложным, и я думаю, что я подхожу к нему в основном в каждом конкретном случае. Поскольку это сложно, я стараюсь, чтобы он был локализован, выбирая не подвергать большие структуры данных пользователям модулей в целом. Обычно это можно сделать, разоблачая комбинаторы для генерации рассматриваемой вещи, а затем создавая и потребляя все это за один раз (примером этого является преобразование четности на монадах).
Моя цель дизайна - заставить каждую функцию уважать образцы данных/кодовых образов моих типов. Я обычно могу ударить его (хотя иногда это требует некоторой тяжелой мысли - это стало естественным на протяжении многих лет), и у меня редко возникают проблемы с утечкой, когда я это делаю. Но я не утверждаю, что это легко - это требует опыта с каноническими библиотеками и образцами языка. Эти решения не принимаются изолированно, и все должно быть правильно сразу для того, чтобы оно хорошо работало. Один плохо настроенный инструмент может испортить весь концерт (поэтому "оптимизация случайным возмущением" почти никогда не работает для этих проблем).
Apfelmus Косвенные инварианты, статья поможет вам дополнительно развивать вашу интуицию space/thunk. Также см. Комментарий Эдварда Кмета.