Это скорее комментарий к ответу @ElBaulP, чем ответ в своем собственном праве, но он слишком велик для комментария.
Я думаю, что вы пропустили корень оптимизации, хотя это явно указано в комментарии выше. Тот факт, что val tail
в constant
равен lazy
, является деталью реализации или, скорее, уловкой, делающей возможным основной источник оптимизации. И основным источником оптимизации является тот факт, что tail
является самоссылочным.
На минутку давайте избавимся от всех вещей lazy
. Предположим, что Cons
определяется как
case class Cons[+A](h: A, t: () => Stream[A]) extends Stream[A]
и давайте определим constant
как
def constant[A](a: A): Stream[A] = {
val tailFunc: () => Stream[A] = () => tail
val tail: Stream[A] = Cons(a, tailFunc)
tail
}
Да, это синтаксически неверная программа, потому что tailFunc
использует tail
, определенный только в следующей строке. Но представьте, что Scala может это скомпилировать. Теперь я думаю, что совершенно ясно, что такая реализация constant
будет создавать только один экземпляр класса Cons
на вызов, и этот экземпляр будет использоваться независимо от того, как долго вы пытаетесь перебрать возвращаемый поток.
Что определяет tail
как lazy
, так это просто сделать код логически эквивалентным вышеупомянутому, скомпилированному без ошибок. С точки зрения реализации это похоже на что-то подобное:
class Lazy[A](var value:A)
def constant[A](a: A): Stream[A] = {
val lazyTail: Lazy[Stream[A]] = new Lazy(null)
// this tailFunc works because lazyTail is already fixed and can be safely
// captured although lazyTail.value will be changed
val tailFunc: () => Stream[A] = () => lazyTail.value
lazyTail.value = new Stream(a, tailFunc)
lazyTail.value
}
Этот код не совсем совпадает с фактической реализацией lazy
во многих деталях, потому что он на самом деле нетерпелив, но я думаю, что это показывает, что вам действительно не нужно lazy
, чтобы заставить оптимизацию работать (но за счет использования var
, что в любом случае делает компилятор Scala в своей реальной, более сложной lazy
реализации).
В вашей наивной реализации
def constant[A](a: A): Stream[A] = Stream.cons(a, constant(a))
ленивая оценка tail
позволяет вам сразу не завершиться с переполнением стека из-за рекурсивных вызовов constant
из самого себя. Но все же, когда вы делаете constant(whatever).tail
, tail
оценивается, поэтому метод constant
вызывается еще раз и создается новый объект Cons
. И это будет происходить каждый раз, когда tail
вызывается на новый head
.
Чтобы переформулировать его еще раз, lazy val tail
- это просто уловка, позволяющая tail
ссылаться на себя при создании, и действительно важной частью является тот факт, что tail
ссылается на себя, что позволяет использовать только один объект для итерация, а не один объект на каждый следующий .tail
вызов.