При написании функции, действующей на Stream
(s), существуют разные понятия рекурсии. Первый простой смысл не является рекурсивным на уровне компилятора, так как хвост, если не оценивается мгновенно, поэтому функция возвращает сразу, но возвращаемый поток рекурсивный:
final def simpleRec[A](as: Stream[A]): Stream[B] =
if (a.isEmpty) Stream.empty
else someB(a.head) #:: simpleRec(a.tail)
Вышеупомянутое понятие рекурсии не вызывает никаких проблем. Второй по сути является хвостовым рекурсивным на уровне компилятора:
@tailrec
final def rec[A](as: Stream[A]): Stream[B] =
if (a.isEmpty) Stream.empty // A) degenerated
else if (someCond) rec(a.tail) // B) tail recursion
else someB(a.head) #:: rec(a.tail) // C) degenerated
Проблема заключается в том, что случай C)
обнаруживается компилятором как вызов без хвоста, даже если фактический вызов не выполняется. Этого можно избежать, разложив хвост потока в вспомогательную функцию:
@tailrec
final def rec[A](as: Stream[A]): Stream[B] =
if (a.isEmpty) Stream.empty
else if (someCond) rec(a.tail) // B)
else someB(a.head) #:: recHelp(a.tail)
@tailrec
final def recHelp[A](as: Stream[A]): Stream[B] =
rec(as)
Пока он компилируется, этот подход в конечном итоге приводит к утечке памяти. Так как хвост-рекурсивный rec
в конечном итоге вызывается из функции recHelp
, кадр стека функции recHelp
содержит ссылку на головку пара и не позволяет потоку собирать мусор до тех пор, пока rec
возвращает вызовы, которые могут быть довольно длинными (с точки зрения шагов рекурсии) в зависимости от количества вызовов B)
.
Обратите внимание, что даже в случае без помощи, если компилятор разрешил @tailrec, утечка памяти может все еще присутствовать, поскольку хвост ленивого потока фактически создает анонимный объект, содержащий ссылку на головку потока.