Как опытные разработчики Haskell подходят к лень в * design * time?

Я - промежуточный программист Haskell с большим опытом работы на строгих языках FP и не-FP. Большая часть моего кода Haskell анализирует умеренно большие наборы данных (10 ^ 6..10 ^ 9 вещей), поэтому лень всегда скрывается. У меня есть достаточно хорошее понимание thunks, WHNF, сопоставление шаблонов и совместного использования, и я смог исправить утечки с помощью шаблонов ударов и seq, но этот подход с профилем и молитвой кажется грубым и неправильным.

Я хочу знать, как опытные программисты Haskell подходят к лень в время разработки. Я не спрашиваю о простых элементах, таких как Data.ByteString.Lazy или foldl '; скорее, я хочу знать, как вы думаете о ленивом аппарате более низкого уровня, который вызывает проблемы с памятью во время выполнения и сложную отладку.

Как вы относитесь к трюкам, сопоставлению шаблонов и совместному использованию во время разработки?

Какие шаблоны дизайна и идиомы вы используете, чтобы избежать утечек?

Как вы узнали эти шаблоны и идиомы, и есть ли у вас хорошие ссылки?

Как избежать преждевременной оптимизации непротекающих проблем?

(Изменен 2014-05-15 для составления бюджета по времени):

Планируете ли вы значительное время проекта для поиска и устранения проблем с памятью?

Или ваши навыки проектирования обычно обходят проблемы памяти, и вы ожидаете потребление памяти в самом начале цикла разработки?

Ответы

Ответ 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. Также см. Комментарий Эдварда Кмета.