Я думаю, что это легче понять, если вы сравните свое определение со следующим:
not_memoized_fib :: Int -> Integer
not_memoized_fib m = map fib [0 ..] !! m
where fib 0 = 0
fib 1 = 1
fib n = not_memoized_fib (n-2) + not_memoized_fib (n-1)
Вышеприведенное определение по сути совпадает с вашим, за исключением того, что оно принимает явный аргумент m
. Это так называемое eta-расширение предыдущей функции, и семантически эквивалентно ей. Тем не менее, в оперативном плане это имеет значительно худшую производительность, поскольку здесь не происходит запоминание.
Почему? Итак, ваша функция определяет список map fib [0..]
до , принимающий (неявный) входной параметр m
, поэтому существует только один список, для всех m
, которые мы можем передать позже в качестве аргументов. Вместо этого в not_memoized_fib
сначала мы принимаем m
в качестве входных данных, а затем определяем список, заставляя функцию создавать список для каждого вызова not_memoized_fib
, снижая производительность.
Это даже легче увидетьесли мы используем let
и лямбды вместо where
. Сравните
memoized :: Int -> Integer
memoized = let
list = map fib [0..]
fib 0 = 0
fib 1 = 1
fib n = memoized (n-1) + memoized (n-2)
in \m -> list !! m
-- ^^ here we take m, after everything is defined,
с его структурой кода lambda (*) , с
not_memoized :: Int -> Integer
not_memoized = \m -> let
-- ^^ here we take m, before everything is defined, so
-- we define local bindings {list,fib} at every call
list = map fib [0..]
fib 0 = 0
fib 1 = 1
fib n = not_memoized (n-1) + not_memoized (n-2)
in list !! m
с символом let внутри лямбда.
В первом случае существует только один список, а во втором - один список для каждого вызова.
(*) a searchable term.