Giorgio
Giorgio

Reputation: 1103

Understand Stream scala interleaved transformations behavior

I'm reading and having fun with examples and exercises contained in the book Functional Programming in Scala. I'm studing the strictess and laziness chapter talking about the Stream.

I can't understand the output produced by the following code excerpt:

sealed trait Stream[+A]{

  def foldRight[B](z: => B)(f: (A, => B) => B): B =   
  this match {
    case Cons(h,t) => f(h(), t().foldRight(z)(f))   
    case _ => z
  }

  def map[B](f: A => B): Stream[B] = foldRight(Stream.empty[B])((h,t) => {println(s"map h:$h"); Stream.cons(f(h), t)})

  def filter(f:A=>Boolean):Stream[A] = foldRight(Stream.empty[A])((h,t) => {println(s"filter h:$h"); if(f(h)) Stream.cons(h,t) else t})
}

case object Empty extends Stream[Nothing]

case class Cons[+A](h: () => A, t: () => Stream[A]) extends Stream[A]   

object Stream {
  def cons[A](hd: => A, tl: => Stream[A]): Stream[A] = {   
    lazy val head = hd   
    lazy val tail = tl
    Cons(() => head, () => tail)
  }
  def empty[A]: Stream[A] = Empty   

  def apply[A](as: A*): Stream[A] =   
    if (as.isEmpty) empty else cons(as.head, apply(as.tail: _*))
}

Stream(1,2,3,4,5,6).map(_+10).filter(_%2==0)

When I execute this code, I receive this output:

map h:1
filter h:11
map h:2
filter h:12

My questions are:

  1. Why map and filter output are interleaved?
  2. Could you explain all steps involved from the Stream creation until the last step for obtaining this behavior?
  3. Where are other elements of the list that pass also filter transformation, so 4 and 6?

Upvotes: 3

Views: 234

Answers (1)

jwvh
jwvh

Reputation: 51271

The key to understanding this behavior, I think, is in the signature of the foldRight.

def foldRight[B](z: => B)(f: (A, => B) => B): B = ...

Note that the 2nd argument, f, is a function that takes two parameters, an A and a by-name (lazy) B. Take away that laziness, f: (A, B) => B, and you not only get the expected method grouping (all the map() steps before all the filter() steps), they also come in reverse order with 6 processed first and 1 processed last, as you'd expect from a foldRight.

How does one little => perform all that magic? It basically says that the 2nd argument to f() is going to be held in reserve until it is required.

So, attempting to answer your questions.

  1. Why map and filter output are interleaved?

Because each call to map() and filter() are delayed until the point when the values are requested.

  1. Could you explain all steps involved from the Stream creation until the last step for obtaining this behavior?

Not really. That would take more time and SO answer space than I'm willing to contribute, but let's take just a few steps into the morass.

We start with a Stream, which looks likes a series of Cons, each holding an Int and a reference to the next Cons, but that's not completely accurate. Each Cons really holds two functions, when invoked the 1st produces an Int and the 2nd produces the next Cons.

Call map() and pass it the "+10" function. map() creates a new function: "Given h and t (both values), create a new Cons. The head function of the new Cons, when invoked, will be the "+10" function applied to the current head value. The new tail function will produce the t value as received." This new function is passed to foldRight.

foldRight receives the new function but the evaluation of the function's 2nd parameter will be delayed until it is needed. h() is called to retrieve the current head value, t() will be called to retrieve the current tail value and a recursive call to foldRight will be called on it.

Call filter() and pass it the "isEven" function. filter() creates a new function: "Given h and t, create a new Cons if h passes the isEven test. If not then return t." That's the real t. Not a promise to evaluate its value later.

  1. Where are other elements of the list that pass also filter transformation, so 4 and 6?

They are still there waiting to be evaluated. We can force that evaluation by using pattern matching to extract the various Cons one by one.

val c0@Cons(_,_) = Stream(1,2,3,4,5,6).map(_+10).filter(_%2==0)
//  **STDOUT**
//map h:1
//filter h:11
//map h:2
//filter h:12

c0.h()  //res0: Int = 12

val c1@Cons(_,_) = c0.t()
//  **STDOUT**
//map h:3
//filter h:13
//map h:4
//filter h:14

c1.h()  //res1: Int = 14

val c2@Cons(_,_) = c1.t()
//  **STDOUT**
//map h:5
//filter h:15
//map h:6
//filter h:16

c2.h()  //res2: Int = 16
c2.t()  //res3: Stream[Int] = Empty

Upvotes: 4

Related Questions