Skip to content

add cycle method to LazyList #17

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

Closed
wants to merge 1 commit into from
Closed
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
49 changes: 49 additions & 0 deletions src/main/scala/scala/collection/immutable/next/package.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/*
* Scala (https://www.scala-lang.org)
*
* Copyright EPFL and Lightbend, Inc.
*
* Licensed under Apache License 2.0
* (http://www.apache.org/licenses/LICENSE-2.0).
*
* See the NOTICE file distributed with this work for
* additional information regarding copyright ownership.
*/

package scala.collection.immutable

package object next {

implicit class NextLazyListExtensions[T](private val ll: LazyList[T]) extends AnyVal {
/**
* When called on a finite `LazyList`, returns a circular structure
* that endlessly repeats the elements in the input.
* The result is a true cycle occupying only constant memory.
*
* Does not force the input list (not even its empty-or-not status).
*
* Safe to call on unbounded input, but in that case the result is not a cycle
* (not even if the input was).
*
* Note that some `LazyList` methods preserve cyclicality and others do not.
* So for example the `tail` of a cycle is still a cycle, but `map` and `filter`
* on a cycle do not return cycles.
*/
def cycle: LazyList[T] =
// case 1: the input is already known to be empty
// (the test can be changed to ll.knownIsEmpty when this code moves to stdlib)
if (ll.knownSize == 0) LazyList.empty
// we don't want to force the input's empty-or-not status until we must.
// `LazyList.empty #:::` accomplishes that delay
else LazyList.empty #::: {
// case 2: the input is later discovered to be empty
if (ll.isEmpty) LazyList.empty
else {
// case 3: non-empty
lazy val result: LazyList[T] = ll #::: result
result
}
}
}

}
120 changes: 120 additions & 0 deletions src/test/scala/scala/collection/immutable/TestLazyListExtensions.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
/*
* Scala (https://www.scala-lang.org)
*
* Copyright EPFL and Lightbend, Inc.
*
* Licensed under Apache License 2.0
* (http://www.apache.org/licenses/LICENSE-2.0).
*
* See the NOTICE file distributed with this work for
* additional information regarding copyright ownership.
*/

package scala.collection.immutable

import org.junit.Assert._
import org.junit.Test

import next._

class TestLazyListExtensions {

// This method will *not* terminate for non-cyclic infinite-sized collections.
// (It's kind of nasty to have tests whose failure mode is to hang, but I don't
// see an obvious alternative that doesn't involve copying code from LazyList.
// Perhaps this could be improved at the time this all gets merged into stdlib.)
def assertConstantMemory[T](xs: LazyList[T]): Unit =
// `force` does cycle detection, so if this terminates, the collection is
// either finite or a cycle
xs.force

@Test
def cycleEmpty1(): Unit = {
val xs = LazyList.empty // realized
val cyc = xs.cycle
assertTrue(cyc.isEmpty)
assertTrue(cyc.size == 0)
assertEquals(Nil, cyc.toList)
}
@Test
def cycleEmpty2(): Unit = {
val xs = LazyList.empty #::: LazyList.empty // not realized
assertEquals(-1, xs.knownSize) // double-check it's not realized
val cyc = xs.cycle
assertTrue(cyc.isEmpty)
assertTrue(cyc.size == 0)
assertEquals(Nil, cyc.toList)
}
@Test
def cycleNonEmpty(): Unit = {
val xs = LazyList(1, 2, 3)
val cyc = xs.cycle
assertFalse(cyc.isEmpty)
assertConstantMemory(cyc)
assertEquals(LazyList(1, 2, 3, 1, 2, 3, 1, 2), cyc.take(8))
}
@Test
def cycleToString(): Unit = {
assertEquals("LazyList()",
LazyList.empty.cycle.toString)
assertEquals("LazyList(<not computed>)",
LazyList(1, 2, 3).cycle.toString)
// note cycle detection here!
assertEquals("LazyList(1, 2, 3, <cycle>)",
LazyList(1, 2, 3).cycle.force.toString)
}
@Test
def cycleRepeats(): Unit = {
val xs = LazyList(1, 2, 3)
val cyc = xs.cycle
assertFalse(cyc.isEmpty)
assertEquals(LazyList(1, 2, 3, 1, 2, 3, 1, 2), cyc.take(8))
}
@Test
def cycleConstantMemory1(): Unit = {
val xs = LazyList(1, 2, 3)
val cyc = xs.cycle
assertTrue(cyc.tail eq cyc.tail.tail.tail.tail)
assertTrue(cyc.tail.tail eq cyc.drop(4).tail)
assertTrue(cyc.tail eq cyc.drop(3).tail)
}
@Test
def cycleConstantMemory2(): Unit = {
var counter = 0
def count(): Int = { counter += 1; counter }
val xs = count() #:: count() #:: count() #:: LazyList.empty
val cyc = xs.cycle
assertEquals(0, counter)
assertEquals(10, cyc.take(10).size)
assertEquals(3, counter)
}
@Test
def cycleConstantMemory3(): Unit = {
val xs = LazyList(1, 2, 3)
val cyc = xs.cycle
assertConstantMemory(cyc)
assertConstantMemory(cyc.tail)
assertConstantMemory(cyc.tail.tail)
assertConstantMemory(cyc.tail.tail.tail)
assertConstantMemory(cyc.tail.tail.tail.tail)
assertConstantMemory(cyc.drop(1))
assertConstantMemory(cyc.drop(10))
}
@Test
def cycleUnbounded(): Unit = {
val xs = LazyList.from(1)
val cyc = xs.cycle
assertEquals(LazyList(1, 2, 3), cyc.take(3))
}
@Test
def cycleSecondCallIsSafeButNotIdempotent(): Unit = {
val xs = LazyList(1, 2, 3)
// this is safe to do
val twice = xs.cycle.cycle
// and the contents are as expected
assertEquals(LazyList(1, 2, 3, 1, 2, 3, 1, 2), twice.take(8))
// but the result is not a cycle. it might be nice if it were, but oh well.
// testing the existing behavior.
assertFalse(twice.tail eq twice.tail.tail.tail.tail)
}
}