4

Like the title says what is the intuition behind recursive algos with streams like:

val fibs: LazyList[Int] = (0 #:: fibs).scanLeft(1)(_ + _)

and

val fibs: LazyList[Int] = 0 #:: 1 #:: (fibs.zip(fibs.tail).map{ t => t._1 + t._2 })

How do they unfold? What is the base case for such algos (if it's Nil, why it's so?) and how do they progress towards fibs.take(5) e.g.?

EDIT.

I do understand there is no base case for a lazily defined Stream, as several people pointed out below. Rather, my question concerns what's the base case when infinite stream gets evaluated like in fibs.take(5)(the answer is Nil I believe, please correct me if I'm wrong) and what are the calculation steps in evaluating fibs.take(5)

Sergey Bushmanov
  • 23,310
  • 7
  • 53
  • 72
  • There is no base case, the recursion is infinite. However, the first `fibs` is weird because it actually doesn't need the recursion, only the `scanLeft` – Luis Miguel Mejía Suárez May 29 '22 at 17:10
  • @LuisMiguelMejíaSuárez `Infinite`. That's right. But don't I/compiler need to start from something? – Sergey Bushmanov May 29 '22 at 17:14
  • The compiler does not need to start from anywhere, it only typechecks. The runtime, starts from `0` and then lazily recurses by demand. But, again, note that your example is a bit weird since the recursion is not really needed. PS: I usually prefer to use `unfold` instead which makes it clearer. – Luis Miguel Mejía Suárez May 29 '22 at 17:35
  • `The runtime, starts from 0` . Why? And I'd appreciate it if you show the first without recursion. – Sergey Bushmanov May 29 '22 at 17:40
  • _"The runtime, starts from 0 . Why? "_ because that is what you code says... is like asking why `println("Hello, world!")` prints `Hello, world!` – Luis Miguel Mejía Suárez May 29 '22 at 17:53
  • About the first one, my bad, the recursion is needed since, otherwise, the `scanLeft` would end instead of being infinite. – Luis Miguel Mejía Suárez May 29 '22 at 17:55
  • I'm not following. How is 0 int related to LazyList? – Sergey Bushmanov May 29 '22 at 18:00
  • `val fibs: LazyList[Int] = (0 #:: fibs).scanLeft(1)(_ + _)` I see a `0` right there, as the head of `fibs` – Luis Miguel Mejía Suárez May 29 '22 at 18:09
  • Right. This is head. What is base case for fibs? And recursion usually unfolds from end to base case. This doesn't seem to be the case here. – Sergey Bushmanov May 29 '22 at 18:10
  • Recursion always start from the head, a traditional recursive algorithm over a `List` will start from the head creating stack frames until reaching the empty list and then it would unfold all those. A tail-recursive algorithm on the other hand would maintain some state to emulate the stack until reaching the empty list and then returning the accumulated state. This is the same here, just that it is infinite but lazy. – Luis Miguel Mejía Suárez May 29 '22 at 18:29
  • `This is the same here`. The same to traditional or tail-recursive? How would I start calculating such streams in imperative way? – Sergey Bushmanov May 29 '22 at 18:41
  • 1
    Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/245151/discussion-between-luis-miguel-mejia-suarez-and-sergey-bushmanov). – Luis Miguel Mejía Suárez May 29 '22 at 18:52

2 Answers2

2

It's say there are 2 things at play here:

  • recursive syntax making use of LazyList API
  • corecursive mathematics behind unfolding

So, let's start with a few words about API and syntax:

  • #:: takes lazy value and prepends it to LazyList definition, here it is fibs which makes its definition recursive on code level
  • LazyList lazily evaluates its arguments and then caches/memoizes them for future use letting us access already computed values immediately

However, the mechanism underneath is actually corecursive.

Let's see what is recursion when it comes to data using List as an example:

List(1,2,3,4)

This can be also written as

1 :: 2 :: 3 :: 4 :: Nil

Which is the same as

( ( ( Nil.::(4) ).::(3) ).::(2) ).::(1)

You can see that we:

  • take Nil
  • create ::(4, Nil) value which we use to
  • create ::(3, ::(4, Nil)) value
  • and so on

In other words, we have to start with some base case and build the whole things from-bottom-up. Such values by definition have to be finite and cannot be used to express series of (possibly) infinite computation.

But there exist an alternative which allows you to express such computations - corecursion and codata.

With corecursion you have a tuple:

  • the last computed value
  • a function which can take the value and return the next tuple (next value + next function!)
  • nothing prevent you from using the same function as second element of the tuple but it's good to have a choice

For instance you could define infinite series of LazyList(1, 2, 3, 4, 5, 6, ...) like:

// I use case class since
//   type Pair = (Int, Int => Pair)
// would be illegal in Scala 
final case class Pair(value: Int, f: Int => Pair)
val f: Int => Pair = n => Pair(n + 1, f)
Pair(1, f)

Then you would take Pair, get value out of it (1 initially) and use it to generate new Pairs (Pair(2, f), Pair(3, f), ...).

Structure which would use corecursion to generate its values would be called codata (so LazyList can be considered codata).

Same story with Fibonacci sequence, you could define it corecursively with

  • (Int, Int) as value (initialized to (0, 1)
  • val f: (Int, Int) => Pair = { case (n, m) => Pair((m, n + m), f } as function
  • finally, you'd have to pick _1 out of every generated (Int, Int) pair

However, LazyList's API gives you some nice tools so that you don't have to do this manually:

  • it memoizes (caches) computed values so you can access list(0), list(1), etc, they aren't forgotten right after use
  • it gives you methods like .map, .flatMap .scanLeft and so on, so while internally it might have more complex types used for corecursion, you are only seeing the final result that you need

Obviously, all of that is done lazily, by codata's definition: at each step you can only know values defined so far, and how to generate next of out it.

That leads us to your example:

val fibs: LazyList[Int] = (0 #:: fibs).scanLeft(1)(_ + _)

You can think of it as something that:

  • starts with a pair (0, f)
  • where the f takes this 0 argument, and combines it with 1 to create (0, 1) tuple
  • and then constructs next fs which trace the previous value, and passes it along current value to the function passed into scanLeft
  • where all the shenanigans with intermediate values and functions and memoization are handled internally by API

So if you asked me, the "base case" of such algos is a pair of value and function returning pair, run over and over again.

Mateusz Kubuszok
  • 24,995
  • 4
  • 42
  • 64
  • Disregarding for now the abstraction of an infinite LazyList, can you show (perhaps with the help of println method), how `fibs.take(5)` evaluates, perhaps like they do [here](https://books.trinket.io/thinkjava2/chapter8.html) with the help of stack diagrams? And show how the `.take(5)` sequence gets constructed explicitly ? – Sergey Bushmanov Jun 05 '22 at 16:09
  • You could but that isn't very "portable" - `LazyList` implements co-recursion, but how it implements it internally it's up to it, and it can implement it differently than Scala 2.12's `Stream`, and differently than Matrioshka's Streams or Droste's streams. In `LazyList`'s case it uses `() => LazyList.State` underneath where state is `Empty` or `Cons`. And the actual computations are done with private tailrec methods which basically optimize to while loops, so at best you would see some trampoline-like things on stack. – Mateusz Kubuszok Jun 06 '22 at 09:57
0

How do they unfold?

They don't. The #:: function takes a by-name argument, which means that it's evaluated lazily.

What is the base case for such algos (if it's Nil, why it's so?).

There is no "base case", these recursive definitions yield infinite streams:

scala> val fibs: LazyList[Int] = (0 #:: fibs).scanLeft(1)(_ + _)
val fibs: LazyList[Int] = LazyList(<not computed>)

scala> fibs.size
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space

(Note the "<not computed>" token, which hints at the laziness)

OlivierBlanvillain
  • 7,701
  • 4
  • 32
  • 51
  • Striclty speaking you're right: they're lazy and infinite. May I rephrase my question as "how do they unfold in case fibs.take(3).force?" e.g. – Sergey Bushmanov May 31 '22 at 07:48
  • As well, by "There is no "base case", these recursive definitions yield infinite streams" do you mean they start evaluation from nothing? – Sergey Bushmanov May 31 '22 at 08:07
  • And `fibs.size` doesn't make sense for a stream, sorry. – Sergey Bushmanov May 31 '22 at 08:11
  • 1
    `0 #:: fibs` defines a stream of size at least one with `0` as its first element, so in that case the stream starts evaluating from `0`. `.take(3).force` will look at the first 3 elements of the stream, `0`, whatever the first element of `fibs` is, and so on and so forth. `.size` can yield something useful on finite streams, e.g. `(0 #:: LazyList.empty).size == 1`. – OlivierBlanvillain May 31 '22 at 08:33
  • `fibs` is defined in terms of fibs, so it begs a question: what is the base case for evaluation (which is `Nil` I believe). So statement "there is no base case" is wrong I believe? – Sergey Bushmanov May 31 '22 at 08:45
  • Your comment seems to pinpoint what's confusing you: when defining an infinite stream there is no base case for evaluation. Unlike lists, which *always* have a `Nil` somewhere down the line, streams don't. – OlivierBlanvillain May 31 '22 at 09:50
  • This is interesting because in other comment I got "A tail-recursive algorithm on the other hand would maintain some state to emulate the stack until reaching the empty list", which I interprete as `Nil` base case? – Sergey Bushmanov May 31 '22 at 11:45
  • But to be clear, I am not talking about infinite stream. Rather, I'am asking what happens with the stream when I do `fibs.take(3)` – Sergey Bushmanov May 31 '22 at 12:07
  • @SergeyBushmanov I explicitly stated multiple times that the recursion on a normal list always starts from the head, not from the `Nil` - That is the same for an infinite `LazyList` it starts for the head and it would never finish it if wouldn't be for laziness; that is the trick, it only evaluates what it needs to evaluate. Check the step by step evaluation I made on the chat discussion. – Luis Miguel Mejía Suárez May 31 '22 at 12:26
  • I think there is some confusion about what is base case and what is returned when base case is reached (when `fibs.take(3)` is evaluated). – Sergey Bushmanov May 31 '22 at 13:21
  • 1
    `fibs.take(3)` is doing a recursion on `n=3` and stops when `n=0`, at which point the implementation completely discards whatever is left of the stream. Maybe you should try writing your own implementation of `take`, that might to clear up your confusion (it's should be a very simple task). – OlivierBlanvillain May 31 '22 at 13:25
  • 1
    @SergeyBushmanov yes, the base case for any recursion on a infinite stream is the empty one _(similar to `Nil`)_ but the algorithm does not need to reach the base case to execute, because, again, is lazy so it can stop whenever it wants. – Luis Miguel Mejía Suárez May 31 '22 at 14:10