Как Haskell обрабатывает список?
Некоторые из нас на работе читают некоторые из Haskell, и мы говорили о некоторых концепциях вчера. Возникает вопрос, что Haskell - ленивый язык, как он обрабатывает извлечение n-го элемента списка?
Например, если бы мы имели
[2,4..10000000] !! 200
Будет ли он заполнять список до 200 элементов? Или он скомпилирует его в уравнение, подобное
n*step + firstValue
а затем вернуть этот расчет? Причина этого в том, что кто-то пытался придумать пример, где программа будет легко исчерпаться из памяти, и мысль об обходе вниз (достаточно большой) список была первым кандидатом, который пришел.
Ответы
Ответ 1
Да, он будет отображать первые 201 элемента списка перед возвратом. Однако, поскольку этот список недоступен нигде в вашей программе, исходные биты будут иметь право на сбор мусора, поскольку он идет вместе, поэтому он будет работать в постоянном пространстве (но в линейном времени) с наивной реализацией.
Конечно, оптимизирующий компилятор может сделать намного лучше. Поскольку ваше выражение является константой, оно может даже оценить его во время компиляции.
Ответ 2
Будет ли он заполнять список до 200 элементов?
В наивной реализации да.
Или он скомпилирует его в уравнение, подобное n*step + firstValue
?
Оптимизирующий компилятор Haskell может это сделать, хотя я бы не ожидал, что фактическая реализация выполнит эту оптимизацию.
Дело в том, что Haskell настолько строго формализован, что можно доказать, что эти два варианта эквивалентны с точки зрения их возвращаемого значения на идеализированной машине, поэтому до компилятора выбрать один из них. Стандарт языка (Haskell Report) просто описывает, какое значение должно быть возвращено, а не как оно должно быть вычислено.
Ответ 3
Термин "лень" имеет точный математический смысл, который вы можете узнать из книг по вызову с помощью лямбда-исчисления. Определение непрофессионала "ничто не оценивается до тех пор, пока результат не понадобится в другом месте" - это всего лишь метафора новичков. Это упрощение, поэтому в сложных ситуациях, подобных этому, требуется понимание всей теории, чтобы объяснить, что происходит.
Точная семантика требует от компилятора не оценивать элементы списка до тех пор, пока не будет выполнена совпадение с ними. Это не вопрос оптимизации - это всегда должно быть так. Так что если вы вычислите!! 3, минимальный размер, который вы получаете (зависит от определения a), следующий:
_: _: _: 5: _
здесь _ Я имею в виду "не оценивается". Вы можете научиться понимать, что оценивается, а что нет, изучая лямбда-исчисление. До тех пор вы можете использовать отладчик GHCi, чтобы увидеть:
Prelude> let l = [1..10]
Prelude> let x = l !! 5
Prelude> :set -fprint-evld-with-show
Prelude> :print x
x = (_t1::Integer)
Prelude> :print l
l = (_t2::[Integer])
Prelude> x
6
Prelude> :print l
l = 1 : 2 : 3 : 4 : 5 : 6 : (_t3::[Integer])
Обратите внимание, что l не оценивается вообще, пока вы не напечатаете x. Печать вызывает шоу, а show выполняет серию сопоставлений с образцами. В этом конкретном случае первые элементы списка оцениваются из-за сопоставления шаблонов внутри реализации [1..10] (фактически он переводится в обычное приложение enumFromTo 1 10
). Однако, если мы добавим m = map (+1) l, отметим, что больше элементов m не оценено, потому что карта имеет меньшее совпадение шаблонов, чем [1..10]:
Prelude> let m = map (+1) l
Prelude> :print m
m = (_t4::[Integer])
Prelude> m !! 5
7
Prelude> :print m
m = (_t5::Integer) : (_t6::Integer) : (_t7::Integer) :
(_t8::Integer) : (_t9::Integer) : 7 : (_t10::[Integer])
Повторяю, можно легко узнать, что оценивается, а что нет, и в какой точной оценке порядка, но вам нужно изучить точную семантику - просто изучение метафоры не позволяет вам понять детали, Последний пример:
> Prelude> let ll = zipWith (+) l (tail l) Prelude> ll !! 5 13 Prelude>
> :print l l = [1,2,3,4,5,6,7,8,9,10]
Таким образом, в зависимости от (статически известной!) структуры вашей программы возможно много ситуаций. По крайней мере, когда вы оцениваете список! 3, вы получите _ : _ : _ : 5 : _
. В самом максимуме вы получаете полный список: 1 : 2 : 3 : 4 : 5 : 6 : 7 : 8 : 9 : 10 : []
.
Я мог бы легко построить все эти 4 примера ситуаций - так что вы тоже можете узнать, но для этого требуется некоторый математический фон.
Ответ 4
Как сказал larsmans, это зависит от компилятора, чтобы решить, что делать. Но я ожидал бы, что GHC заполнит список до 201-го элемента. Но он не будет оценивать эти элементы.
Предполагая, что существует факториальная функция:
factorial n = product [1..n]
Следующий код напечатает факториал 200, он будет создавать первые 201 ячейки списка, но он будет оценивать только один факториал.
print $ [ factorial n | n <- [0,1..] ] !! 201
Ответ 5
Это зависит от x в -Ox
import Criterion.Main
import qualified Data.Vector as V
import qualified Data.List.Stream as S
naive _ = [2,4 .. k] !! n
eq _ = n*2 + 2
uvector _ = V.enumFromThenTo 2 4 k V.! n
stream _ = [2,4 .. k] S.!! n
n = 100000
k = 10*n
main = defaultMain [ bgroup "range"
[ bench "naive" $ whnf naive n
, bench "eq" $ whnf eq n
, bench "uvector" $ whnf uvector n
, bench "stream" $ whnf stream n
]]
-- -Odph -fforce-recomp -fllvm
--
--benchmarking range/naive
--mean: 11.83244 ns, lb 11.39379 ns, ub 12.90468 ns, ci 0.950
--std dev: 3.304705 ns, lb 1.189680 ns, ub 6.155017 ns, ci 0.950
--
--benchmarking range/eq
--mean: 7.911626 ns, lb 7.741035 ns, ub 8.122809 ns, ci 0.950
--std dev: 970.2263 ps, lb 828.3840 ps, ub 1.177933 ns, ci 0.950
--
--benchmarking range/uvector
--mean: 10.74393 ns, lb 10.30107 ns, ub 11.81737 ns, ci 0.950
--std dev: 3.268982 ns, lb 861.2390 ps, ub 5.811662 ns, ci 0.950
--
--benchmarking range/stream
--mean: 12.34206 ns, lb 11.71146 ns, ub 14.07016 ns, ci 0.950
--std dev: 4.959039 ns, lb 2.124692 ns, ub 10.40687 ns, ci 0.950
-- -O3 -fforce-recomp -fasm
--benchmarking range/naive
--mean: 11.11646 ns, lb 10.83341 ns, ub 11.82991 ns, ci 0.950
--std dev: 2.048823 ns, lb 289.9484 ps, ub 3.752569 ns, ci 0.950
--
--benchmarking range/eq
--mean: 8.535535 ns, lb 8.297940 ns, ub 9.067161 ns, ci 0.950
--std dev: 1.771753 ns, lb 933.7552 ps, ub 2.843637 ns, ci 0.950
--
--benchmarking range/uvector
--mean: 11.12599 ns, lb 10.88839 ns, ub 11.71998 ns, ci 0.950
--std dev: 1.734431 ns, lb 306.4149 ps, ub 3.123837 ns, ci 0.950
--
--benchmarking range/stream
--mean: 10.73798 ns, lb 10.42936 ns, ub 11.45102 ns, ci 0.950
--std dev: 2.301690 ns, lb 1.184686 ns, ub 3.877275 ns, ci 0.950
-- -O0 -fforce-recomp -fasm
--benchmarking range/naive
--mean: 1.742292 ms, lb 1.693402 ms, ub 1.934525 ms, ci 0.950
--std dev: 432.1991 us, lb 70.44581 us, ub 1.006263 ms, ci 0.950
--
--benchmarking range/eq
--mean: 37.66248 ns, lb 36.37912 ns, ub 42.66504 ns, ci 0.950
--std dev: 11.91135 ns, lb 1.493463 ns, ub 28.17839 ns, ci 0.950
--
--benchmarking range/uvector
--mean: 36.32181 ms, lb 35.41175 ms, ub 38.63195 ms, ci 0.950
--std dev: 6.887482 ms, lb 2.532232 ms, ub 13.47616 ms, ci 0.950
--
--benchmarking range/stream
--mean: 1.731072 ms, lb 1.692072 ms, ub 1.875080 ms, ci 0.950
--std dev: 342.2325 us, lb 81.77006 us, ub 792.2414 us, ci 0.950
Ну, в этом простом случае GHC (7.0.2) действительно достаточно умный.
Ответ 6
С GHC вы можете писать такие вещи, как
myVal = [2,4..] !! 200
которые ищут элемент в бесконечном списке. Так что, действительно, он не выделяет полный список. См. http://www.haskell.org/haskellwiki/Memory_leak для примера утечек памяти в Haskell.
Ответ 7
Все реализации, о которых я знаю, будут поглаживать список, насколько это необходимо. Действительно, перемещение достаточно большого списка может легко заставить вас бежать из памяти, вам просто нужно организовать его так, чтобы часть, которую вы уже прошли, не могла быть собрана мусором, например.
main :: IO ()
main = do
let xs :: [Int]
xs = [1 .. 10^9]
print (xs !! 123456789)
print (xs !! 2)
компиляция до fromIntegral n*step + start
- сложный бизнес. Это зависит от типа. Если тип элементов списка ограничен, а n
достаточно велик, xs !! n
будет генерировать исключение "слишком большого индекса", но арифметика может быть отлично определена. Таким образом, преобразование будет действительным для Integer
, но не в целом.