diff options
author | Lukas Rytz <lukas.rytz@typesafe.com> | 2014-09-16 11:44:03 +0200 |
---|---|---|
committer | Lukas Rytz <lukas.rytz@typesafe.com> | 2014-09-16 11:44:03 +0200 |
commit | 223e207e5a4904bf9a6bd70972fa69452d228529 (patch) | |
tree | 4f3a0b4200a137a7e44a9c4110fd08b8d7affa91 /src | |
parent | 75f1235bb06185865c54ae811f412c659b82597f (diff) | |
parent | 039f3e3b35f73886f79f00bc578739002fe034cc (diff) | |
download | scala-223e207e5a4904bf9a6bd70972fa69452d228529.tar.gz scala-223e207e5a4904bf9a6bd70972fa69452d228529.tar.bz2 scala-223e207e5a4904bf9a6bd70972fa69452d228529.zip |
Merge pull request #3848 from Ichoran/issue/8680
SI-8680 Stream.addString is too eager
Diffstat (limited to 'src')
-rw-r--r-- | src/library/scala/collection/immutable/Stream.scala | 125 |
1 files changed, 111 insertions, 14 deletions
diff --git a/src/library/scala/collection/immutable/Stream.scala b/src/library/scala/collection/immutable/Stream.scala index 1f97c4c769..91a4e1c43d 100644 --- a/src/library/scala/collection/immutable/Stream.scala +++ b/src/library/scala/collection/immutable/Stream.scala @@ -176,6 +176,12 @@ import scala.language.implicitConversions * loop(1, 1) * } * }}} + * + * Note that `mkString` forces evaluation of a `Stream`, but `addString` does + * not. In both cases, a `Stream` that is or ends in a cycle + * (e.g. `lazy val s: Stream[Int] = 0 #:: s`) will convert additional trips + * through the cycle to `...`. Additionally, `addString` will display an + * un-memoized tail as `?`. * * @tparam A the type of the elements contained in this stream. * @@ -253,12 +259,22 @@ self => * @note Often we use `Stream`s to represent an infinite set or series. If * that's the case for your particular `Stream` then this function will never * return and will probably crash the VM with an `OutOfMemory` exception. + * This function will not hang on a finite cycle, however. * * @return The fully realized `Stream`. */ def force: Stream[A] = { - var these = this - while (!these.isEmpty) these = these.tail + // Use standard 2x 1x iterator trick for cycle detection ("those" is slow one) + var these, those = this + if (!these.isEmpty) these = these.tail + while (those ne these) { + if (these.isEmpty) return this + these = these.tail + if (these.isEmpty) return this + these = these.tail + if (these eq those) return this + those = those.tail + } this } @@ -309,9 +325,24 @@ self => override def toStream: Stream[A] = this - override def hasDefiniteSize = { - def loop(s: Stream[A]): Boolean = s.isEmpty || s.tailDefined && loop(s.tail) - loop(this) + override def hasDefiniteSize: Boolean = isEmpty || { + if (!tailDefined) false + else { + // Two-iterator trick (2x & 1x speed) for cycle detection. + var those = this + var these = tail + while (those ne these) { + if (these.isEmpty) return true + if (!these.tailDefined) return false + these = these.tail + if (these.isEmpty) return true + if (!these.tailDefined) return false + these = these.tail + if (those eq these) return false + those = those.tail + } + false // Cycle detected + } } /** Create a new stream which contains all elements of this stream followed by @@ -690,7 +721,8 @@ self => * `end`. Inside, the string representations of defined elements (w.r.t. * the method `toString()`) are separated by the string `sep`. The method will * not force evaluation of undefined elements. A tail of such elements will be - * represented by a `"?"` instead. + * represented by a `"?"` instead. A cyclic stream is represented by a `"..."` + * at the point where the cycle repeats. * * @param b The [[collection.mutable.StringBuilder]] factory to which we need * to add the string elements. @@ -701,16 +733,81 @@ self => * resulting string. */ override def addString(b: StringBuilder, start: String, sep: String, end: String): StringBuilder = { - def loop(pre: String, these: Stream[A]) { - if (these.isEmpty) b append end - else { - b append pre append these.head - if (these.tailDefined) loop(sep, these.tail) - else b append sep append "?" append end + b append start + if (!isEmpty) { + b append head + var cursor = this + var n = 1 + if (cursor.tailDefined) { // If tailDefined, also !isEmpty + var scout = tail + if (scout.isEmpty) { + // Single element. Bail out early. + b append end + return b + } + if ((cursor ne scout) && scout.tailDefined) { + cursor = scout + scout = scout.tail + // Use 2x 1x iterator trick for cycle detection; slow iterator can add strings + while ((cursor ne scout) && scout.tailDefined) { + b append sep append cursor.head + n += 1 + cursor = cursor.tail + scout = scout.tail + if (scout.tailDefined) scout = scout.tail + } + } + if (!scout.tailDefined) { // Not a cycle, scout hit an end + while (cursor ne scout) { + b append sep append cursor.head + n += 1 + cursor = cursor.tail + } + } + else { + // Cycle. + // If we have a prefix of length P followed by a cycle of length C, + // the scout will be at position (P%C) in the cycle when the cursor + // enters it at P. They'll then collide when the scout advances another + // C - (P%C) ahead of the cursor. + // If we run the scout P farther, then it will be at the start of + // the cycle: (C - (P%C) + (P%C)) == C == 0. So if another runner + // starts at the beginning of the prefix, they'll collide exactly at + // the start of the loop. + var runner = this + var k = 0 + while (runner ne scout) { + runner = runner.tail + scout = scout.tail + k += 1 + } + // Now runner and scout are at the beginning of the cycle. Advance + // cursor, adding to string, until it hits; then we'll have covered + // everything once. If cursor is already at beginning, we'd better + // advance one first unless runner didn't go anywhere (in which case + // we've already looped once). + if ((cursor eq scout) && (k > 0)) { + b append sep append cursor.head + n += 1 + cursor = cursor.tail + } + while (cursor ne scout) { + b append sep append cursor.head + n += 1 + cursor = cursor.tail + } + // Subtract prefix length from total length for cycle reporting. + // (Not currently used, but probably a good idea for the future.) + n -= k + } + } + if (!cursor.isEmpty) { + // Either undefined or cyclic; we can check with tailDefined + if (!cursor.tailDefined) b append sep append "?" + else b append sep append "..." } } - b append start - loop("", this) + b append end b } |