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


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

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

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

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

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

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

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

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

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

1 40

1 ответ:

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

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

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

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

iterate' :: (a -> a) -> a -> [a]
iterate' f x = x : (x `seq` iterate' f (f x))
Когда вы начинаете сочинять вещи, конечно, становится все труднее определить, когда случаются плохие случаи. В общем, трудно создать эффективные структуры данных / функции, которые одинаково хорошо работают с данными и кодатами, и важно помнить, что есть что (даже в полиморфной среде, где это не гарантировано, вы должны иметь один в виду и стараться уважать его). Обмен информацией-сложная штука, и я думаю, что подхожу к ней в основном на индивидуальной основе. Потому что это сложно, я старайтесь держать его локализованным, предпочитая не предоставлять большие структуры данных пользователям модулей в целом. Обычно это можно сделать, выставив комбинаторы для генерации рассматриваемой вещи, а затем производя и потребляя все это за один раз (преобразование коденсности на монадах является примером этого).

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

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