Reputation: 3446
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
?
Upvotes: 1
Views: 545
Reputation: 25950
There are two things to consider :
acc
p(x)
in the boolean expression.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 i.e. 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.
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.
Now what happens if one of your operands is a lazy expression ? Well, if you have 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
.
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
Upvotes: 1