openlang

foldl прекрасно компонуется и всё равно течёт

foldl (+) 0 на длинном списке это самая маленькая утечка памяти, какую я знаю, и дело в ней скорее в композиции, чем в арифметике. На каждом шаге строится санк (acc + x), который никто не форсирует до самого конца, и аккумулятор превращается в цепочку длиной со список. foldl' из Data.List на каждом шаге приводит аккумулятор к слабой заголовочной нормальной форме, и цепочка не возникает.

Ловушка в том, что у обеих версий один и тот же тип. В foldl :: Foldable t => (b -> a -> b) -> b -> t a -> b ничего не сказано о том, сколько памяти держит результат, пока его не потребовали. Заверните свёртку в симпатичный комбинатор, и решение о строгости уйдёт ещё на уровень глубже, туда, где читатель его уже не видит.

С включённой оптимизацией анализ строгости в GHC часто спасает и обычный foldl. Именно поэтому в GHCi это застаёт врасплох.

Хуже того, foldl' доходит только до WHNF. Накапливайте пару (sum, count), и вычислится лишь конструктор пары, а оба поля останутся ленивыми. Нужны строгие поля или bang-паттерны на компонентах.

Поэтому строгость для меня больше не деталь оптимизации. Для комбинатора, который сворачивает, ей место в имени или в документации, рядом с типом.

Разговор продолжается 4

Следуя доводам
  1. Ссылка на ответ

    Прежде чем выкладывать исправление, скажите, что именно измеряли. Общий объём выделенной памяти и пиковое потребление это разные величины, и утечка видна во второй. Строгая свёртка может выделять примерно столько же, сколько ленивая, почти ничего не удерживая живым. Для GHC это значит смотреть maximum residency в статистике -s, а не строку о выделении над ней. tracemalloc.get_traced_memory() в Python по той же причине возвращает текущее и пиковое значение отдельно.

  2. Ссылка на ответ

    Одной максимальной резидентности для исправления мало. Она говорит, что утечка есть, но не говорит, из чего она состоит. Поэтому сначала резидентность, потом профиль кучи по типам замыканий, -hT, он работает без профилировочной сборки, и санки сразу будут видны. На JVM правило то же. Трогать хоть один флаг сборщика можно только после этого. Настройка сборщика под цепочку санков лишь делает утечку удобнее.

  3. Ссылка на ответ

    Имена и документация это не удержат. В SQL никто даже не называет это ленивостью, а что на самом деле выполнится из составного представления, решает планировщик. PostgreSQL встраивает нерекурсивное CTE без побочных эффектов, на которое ссылаются один раз, если не написать MATERIALIZED, так что с версии 12 обёртка подзапроса в WITH больше не служит барьером оптимизации. Текст запроса тот же, вычисление другое, и ни один комментарий в документации этого не заметил. Пример ilse с GHCi показывает то же самое для GHC. Запустите EXPLAIN. Порядок вычисления живёт там.

  4. Ссылка на ответ

    Можно дать аккумулятору собственный тип со строгими полями, что-то вроде data Acc = Acc !Int !Int. Тогда foldl', форсируя внешний конструктор, форсирует и оба счётчика, а строгость записана один раз, там, где определены данные, а не в каждой свёртке. Пожалуй, для большинства таких комбинаторов этого достаточно.