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

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

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

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

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

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

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

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

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

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

1 ответ

Решение

Я думаю, что большинство проблем с "утечками строгости" происходит потому, что у людей нет хорошей концептуальной модели. Хаскелеры без хорошей концептуальной модели, как правило, имеют и распространяют суеверия о том, что чем строже, тем лучше. Возможно, эта интуиция проистекает из того, что они играют с маленькими примерами и узкими петлями. Но это неверно. Столь же важно быть ленивым в нужное время и строгим в нужное время.

Существует два лагеря типов данных, обычно называемых "данными" и "кодатами". Важно уважать шаблоны каждого из них.

  • Операции, которые производят "данные" (Int, ByteString, ...), должны быть принудительно близки к тому месту, где они происходят. Если я добавляю число в аккумулятор, я стараюсь убедиться, что оно будет принудительно введено, прежде чем я добавлю еще один. Хорошее понимание лени здесь очень важно, особенно его условный характер (то есть суждения строгости не принимают форму "X оценивается "а скорее",когда Y оценивается, так это X").
  • Операции, которые производят и потребляют "кодаты" (большую часть времени содержит списки, деревья, большинство других рекурсивных типов), должны делать это постепенно. Обычно преобразование codata -> codata должно генерировать некоторую информацию для каждого бита информации, которую они потребляют (по модулю пропускается как filter). Еще одна важная часть для codata - это то, что вы используете ее линейно, когда это возможно, то есть используйте конец списка ровно один раз; используйте каждую ветку дерева ровно один раз. Это гарантирует, что GC может собирать куски по мере их потребления.

Вещи проявляют особую осторожность, когда у вас есть кодата, содержащая данные. Например iterate (+1) 0 !! 1000 закончится тем, что произведет размер 1000, прежде чем оценивать его. Вам нужно снова подумать об условной строгости - способ предотвратить этот случай - убедиться, что при использовании минусов списка происходит добавление его элемента. iterate нарушает это, поэтому нам нужна лучшая версия.

iterate' :: (a -> a) -> a -> [a]
iterate' f x = x : (x `seq` iterate' f (f x))

Когда вы начинаете сочинять вещи, конечно, становится сложнее сказать, когда случаются плохие дела. В общем, сложно создать эффективные структуры данных / функции, которые одинаково хорошо работают с данными и кодатами, и важно помнить, что есть что (даже в полиморфной обстановке, где это не гарантировано, вы должны иметь это в виду и попробовать уважать это).

Совместное использование сложно, и я думаю, что я подхожу к нему в основном на индивидуальной основе. Поскольку это сложно, я стараюсь держать его локализованным, предпочитая не предоставлять большие структуры данных пользователям модуля в целом. Обычно это можно сделать, выставив комбинаторы для генерации рассматриваемой вещи, а затем производя и потребляя все это за один раз (например, преобразование кодирования на монадах).

Моя цель состоит в том, чтобы каждая функция уважала шаблоны данных / кодатов моих типов. Я обычно могу поразить его (хотя иногда это требует некоторых серьезных размышлений - это стало естественным с годами), и у меня редко бывают проблемы с утечками, когда я делаю. Но я не утверждаю, что это легко - это требует опыта работы с каноническими библиотеками и шаблонами языка. Эти решения не принимаются изолированно, и все должно быть правильно, чтобы оно работало хорошо. Один плохо настроенный инструмент может испортить весь концерт (вот почему "оптимизация по случайному возмущению" почти никогда не работает для такого рода проблем).

Статья Апфельма " Космические инварианты" полезна для дальнейшего развития вашей космической интуиции. Также см. Комментарий Эдварда Кметта ниже.

Другие вопросы по тегам