Kevin Meredith
Kevin Meredith

Reputation: 41909

Implementing Stream.toList

Here's my attempt at Functional Programming in Scala's exercise to write toList for the Stream class.

def toList[A](stream: Stream[A]): List[A] = {
    def go(s: Stream[A], acc: List[A]): List[A] = s match {
        case x #:: xs => go(xs, acc :+ x)
        case _ => acc
    }
    go(stream, Nil)
}

Based on reading (but not understanding all) of this post, I was unsure if my pattern matching was correct. In particular, I was concerned that my first case was resulting in immediately evaluating the tail of stream.

Conceptually, I think that I need to implement toList where each recursion step adds the head of the stream to the list, not evaluating the tail for each step.

Am I correct in this understanding and the above implementation?

Upvotes: 2

Views: 361

Answers (1)

Brian
Brian

Reputation: 20285

The docs and source for Stream addresses this pretty well.

The source for #:: is:

  object #:: {
    def unapply[A](xs: Stream[A]): Option[(A, Stream[A])] =
      if (xs.isEmpty) None
      else Some((xs.head, xs.tail))
  }

The relevant part of this is Some((xs.head, xs.tail)). The source for tail states that:

Note that this method does not force evaluation of the Stream but merely returns the lazy result.

Based on the docs xs does not get evaluated in the case statement case x #:: xs. If this case matches, then go(xs, acc :+ x) is called. xs here is essentially s.tail which is not evaluated as noted above.

The code in the post you linked to is different in that tail( a Stream) is used in recursive calls to merge and is then unapply is called on it destructuring it into it's head and tail. Since the head is a Stream it gets evaluated and will stack overflow on large enough inputs. @Didier Dupont states this nicely too:

Note also that in Scala Stream, while the tail is lazy, the head is not. When you have a (non empty) Stream, the head has to be known. Which means that when you get the tail of the stream, itself a stream, its head, that is the second element of the original stream, has to be computed. This is sometimes problematic, but in your example, you fail before even getting there.

The extraction of head and tail for xs in your code won't have this issue unless A is a Stream. i.e. a stream of streams. The code in the other post creates this situation via the recursive call merge.

Upvotes: 4

Related Questions