Как упоминалось в комментариях, программа Haskell, которая выглядит так, будто она генерирует «дополнительный список», редко генерирует дополнительный список (благодаря ленивой оценке, среди прочего), поэтому вряд ли будет добавлено дополнительное использование памяти с использованием zip3
. Если функция используется таким образом, что список может быть потреблен по мере его создания, фактические списки (промежуточные или конечные) не будут произведены.
Для примера рассмотрим следующую Scan.hs
программу, которая потребляет большой список с sum
, как это производится:
import Data.List (scanl')
scanl3 :: (acc -> (a, b, c) -> acc) -> acc -> [a] -> [b] -> [c] -> [acc]
scanl3 f z as bs cs = scanl' f z $ zip3 as bs cs
main = print $ sum $ scanl3 (\z (a, b, c) -> z + a*b + 2*c)
0 [1..100000000] [10,20..] [100,200..]
При компиляции это для профилирования с -O2
оптимизаций:
ghc -prof -rtsopts=all -O2 Scan.hs
затем запустить с профилированием кучи:
./Scan +RTS -h
hp2ps Scan.hp
evince Scan.ps
вы обнаружите, что он работает в постоянной памяти около 35k), не создавая ни промежуточный список zip3
, ни окончательный список scanl3
явно.
Если scanl3
используется в условиях, когда весь список должен быть проведен в памяти сразу, промежуточный список еще не должно вызывать увеличение использования памяти, хотя единственный способ, чтобы быть уверенным в том, чтобы профиль как это, так и альтернативную реализацию в конкретном случае использования.
Что касается временных затрат на использование zip3
в качестве промежуточного шага, вам действительно нужно провести сравнение с альтернативной реализацией, чтобы быть уверенным. Я сравнил выше версии с помощью zip3
против рекурсивной версии:
scanl3 :: (acc -> (a, b, c) -> acc) -> acc -> [a] -> [b] -> [c] -> [acc]
scanl3 f z (a:as) (b:bs) (c:cs) = z : scanl3 f (f z (a, b, c)) as bs cs
scanl3 _ z _ _ _ = [z]
, используя тот же тестовый пример, как описано выше собран с -O2
и обнаружил, что во время выполнения, чтобы быть почти идентичны (15.2sec для zip3
против 16.8sec для рекурсивной).
Обратите внимание, что я явно не сделал рекурсивную версию строгой, но в конкретном случае использования (суммируя ее по мере ее создания) это не имеет никакого значения.
Редактировать: На самом деле, я думаю, это имеет значение. Строгий вариант с использованием шаблона Взрыва:
{-# LANGUAGE BangPatterns #-}
scanl3 :: (acc -> (a, b, c) -> acc) -> acc -> [a] -> [b] -> [c] -> [acc]
scanl3 f !z (a:as) (b:bs) (c:cs) = z : scanl3 f (f z (a, b, c)) as bs cs
scanl3 _ z _ _ _ = [z]
выглядит, как он может работать последовательно 0,5-1,0 секунды быстрее (из 16secs).
zip3 не создает дополнительный список, потому что он конструирует его ленивым способом. Накладные расходы - только в значениях pack/unpack в/из тройки. Однако это выражение будет оцениваться в постоянном пространстве памяти (если as, bs и cs list уже построены или их никто не сохраняет). – freestyle
@freestyle Это немного преувеличение: в то время как список создается лениво, каждая ячейка cons сохраняется и эта конструкция и деконструкция внутри 'scanl'' имеют некоторые накладные расходы в дополнение к источникам, которые вы перечисляете (если только не происходит слияние списков). –