Skip to content

Add lazyFoldLeft #18

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jul 30, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions src/main/scala/scala/collection/decorators/IterableDecorator.scala
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,21 @@ class IterableDecorator[C, I <: IsIterable[C]](coll: C)(implicit val it: I) {
def foldSomeLeft[B](z: B)(op: (B, it.A) => Option[B]): B =
it(coll).iterator.foldSomeLeft(z)(op)

/** Lazy left to right fold. Like `foldLeft` but the combination function `op` is
* non-strict in its second parameter. If `op(b, a)` chooses not to evaluate `a` and
* returns `b`, this terminates the traversal early.
*
* @param z the start value
* @param op the binary operator
* @tparam B the result type of the binary operator
* @return the result of inserting `op` between consecutive elements of the
* collection, going left to right with the start value `z` on the left,
* and stopping when all the elements have been traversed or earlier if
* `op(b, a)` choose not to evaluate `a` and returns `b`
*/
def lazyFoldLeft[B](z: B)(op: (B, => it.A) => B): B =
it(coll).iterator.lazyFoldLeft(z)(op)

/**
* Right to left fold that can be interrupted before traversing the whole collection.
* @param z the start value
Expand Down
14 changes: 14 additions & 0 deletions src/main/scala/scala/collection/decorators/IteratorDecorator.scala
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,20 @@ class IteratorDecorator[A](val `this`: Iterator[A]) extends AnyVal {
result
}

def lazyFoldLeft[B](z: B)(op: (B, => A) => B): B = {
var result = z
var finished = false
while (`this`.hasNext && !finished) {
var nextEvaluated = false
val elem = `this`.next()
def getNext = { nextEvaluated = true; elem }
val acc = op(result, getNext)
finished = !nextEvaluated && acc == result
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is not clear to me why we have the && acc == result part.

This makes the following never terminate, although no elements are evaluated!

Iterator.from(0).lazyFoldLeft(true)((b, _) => !b)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need it to deal with folds such as:

def length(as: Iterable[Int]): Int =
  as.lazyFoldLeft(0)((b, _) => b + 1)

In this case we aren't evaluating the list element but we need to continue the fold because the accumulator is changing. Otherwise we wouldn't correctly compute the length of the list.

In the example you gave, I think non-termination is the correct behavior. Every iteration the result alternates between true and false so it continues forever. Note that these are the same results we would get if we used a naive implementation of foldRight:

def foldRight[A, B](as: Iterable[A])(z: => B)(f: (A, => B) => B): B =
  if (as.isEmpty) z
  else f(as.head, foldRight(as.tail)(z)(f))

This would correctly compute the length of a list and not terminate on your example (technically it would fail with a stack overflow error).

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the explanation, that makes sense to me!

result = acc
}
result
}

def lazyFoldRight[B](z: B)(op: A => Either[B, B => B]): B = {

def chainEval(x: B, fs: immutable.List[B => B]): B =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,24 @@ class IterableDecoratorTest {
Assert.assertEquals(10, List[Int]().foldSomeLeft(10)((x, y) => Some(x + y)))
}

@Test
def lazyFoldLeftIsStackSafe(): Unit = {
val bigList = List.range(1, 50000)
def sum(as: Iterable[Int]): Int =
as.lazyFoldLeft(0)(_ + _)

Assert.assertEquals(sum(bigList), 1249975000)
}

@Test
def lazyFoldLeftIsLazy(): Unit = {
val nats = LazyList.from(0)
def exists[A](as: Iterable[A])(f: A => Boolean): Boolean =
as.lazyFoldLeft(false)(_ || f(_))

Assert.assertTrue(exists(nats)(_ > 100000))
}

@Test def lazyFoldRightIsLazy(): Unit = {
val xs = LazyList.from(0)
def chooseOne(x: Int): Either[Int, Int => Int]= if (x < (1 << 16)) Right(identity) else Left(x)
Expand Down