Skip to content

Commit 1348e32

Browse files
committed
add cycle method to LazyList
1 parent 14a42c0 commit 1348e32

File tree

2 files changed

+169
-0
lines changed

2 files changed

+169
-0
lines changed
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
/*
2+
* Scala (https://www.scala-lang.org)
3+
*
4+
* Copyright EPFL and Lightbend, Inc.
5+
*
6+
* Licensed under Apache License 2.0
7+
* (http://www.apache.org/licenses/LICENSE-2.0).
8+
*
9+
* See the NOTICE file distributed with this work for
10+
* additional information regarding copyright ownership.
11+
*/
12+
13+
package scala.collection.immutable
14+
15+
package object next {
16+
17+
implicit class NextLazyListExtensions[T](private val ll: LazyList[T]) extends AnyVal {
18+
/**
19+
* When called on a finite `LazyList`, returns a circular structure
20+
* that endlessly repeats the elements in the input.
21+
* The result is a true cycle occupying only constant memory.
22+
*
23+
* Does not force the input list (not even its empty-or-not status).
24+
*
25+
* Safe to call on unbounded input, but in that case the result is not a cycle
26+
* (not even if the input was).
27+
*
28+
* Note that some `LazyList` methods preserve cyclicality and others do not.
29+
* So for example the `tail` of a cycle is still a cycle, but `map` and `filter`
30+
* on a cycle do not return cycles.
31+
*/
32+
def cycle: LazyList[T] =
33+
// case 1: the input is already known to be empty
34+
// (the test can be changed to ll.knownIsEmpty when this code moves to stdlib)
35+
if (ll.knownSize == 0) LazyList.empty
36+
// we don't want to force the input's empty-or-not status until we must.
37+
// `LazyList.empty #:::` accomplishes that delay
38+
else LazyList.empty #::: {
39+
// case 2: the input is later discovered to be empty
40+
if (ll.isEmpty) LazyList.empty
41+
else {
42+
// case 3: non-empty
43+
lazy val result: LazyList[T] = ll #::: result
44+
result
45+
}
46+
}
47+
}
48+
49+
}
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
/*
2+
* Scala (https://www.scala-lang.org)
3+
*
4+
* Copyright EPFL and Lightbend, Inc.
5+
*
6+
* Licensed under Apache License 2.0
7+
* (http://www.apache.org/licenses/LICENSE-2.0).
8+
*
9+
* See the NOTICE file distributed with this work for
10+
* additional information regarding copyright ownership.
11+
*/
12+
13+
package scala.collection.immutable
14+
15+
import org.junit.Assert._
16+
import org.junit.Test
17+
18+
import next._
19+
20+
class TestLazyListExtensions {
21+
22+
// This method will *not* terminate for non-cyclic infinite-sized collections.
23+
// (It's kind of nasty to have tests whose failure mode is to hang, but I don't
24+
// see an obvious alternative that doesn't involve copying code from LazyList.
25+
// Perhaps this could be improved at the time this all gets merged into stdlib.)
26+
def assertConstantMemory[T](xs: LazyList[T]): Unit =
27+
// `force` does cycle detection, so if this terminates, the collection is
28+
// either finite or a cycle
29+
xs.force
30+
31+
@Test
32+
def cycleEmpty1(): Unit = {
33+
val xs = LazyList.empty // realized
34+
val cyc = xs.cycle
35+
assertTrue(cyc.isEmpty)
36+
assertTrue(cyc.size == 0)
37+
assertEquals(Nil, cyc.toList)
38+
}
39+
@Test
40+
def cycleEmpty2(): Unit = {
41+
val xs = LazyList.empty #::: LazyList.empty // not realized
42+
assertEquals(-1, xs.knownSize) // double-check it's not realized
43+
val cyc = xs.cycle
44+
assertTrue(cyc.isEmpty)
45+
assertTrue(cyc.size == 0)
46+
assertEquals(Nil, cyc.toList)
47+
}
48+
@Test
49+
def cycleNonEmpty(): Unit = {
50+
val xs = LazyList(1, 2, 3)
51+
val cyc = xs.cycle
52+
assertFalse(cyc.isEmpty)
53+
assertConstantMemory(cyc)
54+
assertEquals(LazyList(1, 2, 3, 1, 2, 3, 1, 2), cyc.take(8))
55+
}
56+
@Test
57+
def cycleToString(): Unit = {
58+
assertEquals("LazyList()",
59+
LazyList.empty.cycle.toString)
60+
assertEquals("LazyList(<not computed>)",
61+
LazyList(1, 2, 3).cycle.toString)
62+
// note cycle detection here!
63+
assertEquals("LazyList(1, 2, 3, <cycle>)",
64+
LazyList(1, 2, 3).cycle.force.toString)
65+
}
66+
@Test
67+
def cycleRepeats(): Unit = {
68+
val xs = LazyList(1, 2, 3)
69+
val cyc = xs.cycle
70+
assertFalse(cyc.isEmpty)
71+
assertEquals(LazyList(1, 2, 3, 1, 2, 3, 1, 2), cyc.take(8))
72+
}
73+
@Test
74+
def cycleConstantMemory1(): Unit = {
75+
val xs = LazyList(1, 2, 3)
76+
val cyc = xs.cycle
77+
assertTrue(cyc.tail eq cyc.tail.tail.tail.tail)
78+
assertTrue(cyc.tail.tail eq cyc.drop(4).tail)
79+
assertTrue(cyc.tail eq cyc.drop(3).tail)
80+
}
81+
@Test
82+
def cycleConstantMemory2(): Unit = {
83+
var counter = 0
84+
def count(): Int = { counter += 1; counter }
85+
val xs = count() #:: count() #:: count() #:: LazyList.empty
86+
val cyc = xs.cycle
87+
assertEquals(0, counter)
88+
assertEquals(10, cyc.take(10).size)
89+
assertEquals(3, counter)
90+
}
91+
@Test
92+
def cycleConstantMemory3(): Unit = {
93+
val xs = LazyList(1, 2, 3)
94+
val cyc = xs.cycle
95+
assertConstantMemory(cyc)
96+
assertConstantMemory(cyc.tail)
97+
assertConstantMemory(cyc.tail.tail)
98+
assertConstantMemory(cyc.tail.tail.tail)
99+
assertConstantMemory(cyc.tail.tail.tail.tail)
100+
assertConstantMemory(cyc.drop(1))
101+
assertConstantMemory(cyc.drop(10))
102+
}
103+
@Test
104+
def cycleUnbounded(): Unit = {
105+
val xs = LazyList.from(1)
106+
val cyc = xs.cycle
107+
assertEquals(LazyList(1, 2, 3), cyc.take(3))
108+
}
109+
@Test
110+
def cycleSecondCallIsSafeButNotIdempotent(): Unit = {
111+
val xs = LazyList(1, 2, 3)
112+
// this is safe to do
113+
val twice = xs.cycle.cycle
114+
// and the contents are as expected
115+
assertEquals(LazyList(1, 2, 3, 1, 2, 3, 1, 2), twice.take(8))
116+
// but the result is not a cycle. it might be nice if it were, but oh well.
117+
// testing the existing behavior.
118+
assertFalse(twice.tail eq twice.tail.tail.tail.tail)
119+
}
120+
}

0 commit comments

Comments
 (0)