简体   繁体   中英

Scala Stream function evaluation

I got a following code:

trait Stream[+A] {
  def uncons: Option[(A, Stream[A])]
  def foldRight[B](z: => B)(f: (A, => B) => B): B = {
    uncons.map(t => {
      f(t._1, t._2.foldRight(z)(f))
    }).getOrElse(z)
  }
  def exists(p: A => Boolean) =
    foldRight(false)((x, acc) => acc || p(x))
  def forAll(p: A => Boolean) =
    foldRight(true)((x, acc) => p(x) && acc)
}

object Stream {
  def cons[A](h: => A, t: => Stream[A]): Stream[A] =
    new Stream[A] {
      lazy val uncons = Some((h, t))
    }
}

Then I create a Stream in a lazy manner and invoke exists method to check what stream elements were evaluated:

  println(Stream.cons({println("5"); 1}, Stream.cons({println("6"); 2}, Stream.cons({println("7"); 3}, Stream.cons({println("8"); 4}, Stream.empty)))).exists(_ == 1))

And what I see is:

5
6
7
8
true

So all the elements were evaluated in spite of only first one would be enough. I seem to understand why exists acts the way it does.

Then I run the following code:

println(Stream.cons({println("13"); 1}, Stream.cons({println("14"); 2}, Stream.cons({println("15"); 3}, Stream.cons({println("16"); 4}, Stream.empty)))).forAll(_ < 2))

and see the following:

13
14
false

So as far as forAll comes across a non-satisfying value it terminates the traversal.

But why forAll acts that way? What's the crucial difference between it and exists ?

There are two things to consider :

  • the type of acc
  • the order of p(x) in the boolean expression.

Laziness

If you change the type of acc to B , you won't be able to fail-fast (or short-circuit) in either of your methods. You must know it since your code extensively uses laziness, but a variable of type => B will get evaluated only when its value is required ie used in some expression. In this case, acc is the future of the result computed over the stream. This future will happen only if you try looking at it. Thus, to prevent the whole stream to be evaluated, you must prevent this future to be looked at.

Short-circuiting in boolean expressions

This is where the order of p(x) matters. In the expression a && b , if a is false then we know the whole conjunction is also false , thus Scala won't try evaluating b because it's pointless.

Combining the two

Now what happens if one of your operands is a lazy expression ? Well, if you have lazyA || b lazyA || b , Scala will read the expression from left to right and evaluate lazyA . In your case, lazyA represents the accumulation of the next element and the rest of the stream. Thus, lazyA expands to a0 :: lazyA1 , which expands to a0 :: a1 :: lazyA2 . You will therefore end up computing the whole stream just for computing the left part of your boolean binop.

Now, if you have a && lazyB , this expands to a && (b0 :: b1 :: lazyB2) . As you see here, as soon as a or bi is false , this will return without evaluating the right part of the statement. This is what happens in your forAll .

How to fix it

The good news is that the fix is very easy : just swap the order of p(x) and acc : as soon as p(x) is true , the disjunction will return without evaluating acc , stopping the computation.

def exists(p: A => Boolean) = foldRight(false)((x, acc) => p(x) || acc)

Output :

5
true

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM