From 094f5bf3fa1e5faafd31e661d082447f3597d70a Mon Sep 17 00:00:00 2001 From: Julien Richard-Foy Date: Tue, 20 Dec 2016 14:16:49 +0100 Subject: [PATCH 1/2] Add non-empty collections --- .../scala/strawman/collection/Iterable.scala | 7 +- src/main/scala/strawman/collection/Seq.scala | 69 +++++++++++++------ .../collection/immutable/LazyList.scala | 4 +- .../strawman/collection/immutable/List.scala | 33 ++++++--- .../collection/mutable/Iterator.scala | 5 +- .../scala/strawman/collection/test/Test.scala | 18 +---- 6 files changed, 82 insertions(+), 54 deletions(-) diff --git a/src/main/scala/strawman/collection/Iterable.scala b/src/main/scala/strawman/collection/Iterable.scala index 079adcda33..dd2814d5dc 100644 --- a/src/main/scala/strawman/collection/Iterable.scala +++ b/src/main/scala/strawman/collection/Iterable.scala @@ -2,7 +2,7 @@ package strawman.collection import scala.annotation.unchecked.uncheckedVariance import scala.reflect.ClassTag -import scala.{Int, Boolean, Array, Any, Unit, StringContext} +import scala.{Int, Boolean, Array, Any, Unit, StringContext, Option} import java.lang.String import strawman.collection.mutable.{ArrayBuffer, StringBuilder} @@ -69,8 +69,7 @@ trait IterableOps[+A] extends Any { /** Is the collection empty? */ def isEmpty: Boolean = !iterator().hasNext - /** The first element of the collection. */ - def head: A = iterator().next() + def headOption: Option[A] = iterator().headOption /** The number of elements in this collection, if it can be cheaply computed, * -1 otherwise. Cheaply usually means: Not requiring a collection traversal. @@ -165,8 +164,6 @@ trait IterableMonoTransforms[+A, +Repr] extends Any { */ def drop(n: Int): Repr = fromIterableWithSameElemType(View.Drop(coll, n)) - /** The rest of the collection without its first element. */ - def tail: Repr = drop(1) } /** Transforms over iterables that can return collections of different element types. diff --git a/src/main/scala/strawman/collection/Seq.scala b/src/main/scala/strawman/collection/Seq.scala index f28a9849f0..9847271c6f 100644 --- a/src/main/scala/strawman/collection/Seq.scala +++ b/src/main/scala/strawman/collection/Seq.scala @@ -1,6 +1,6 @@ package strawman.collection -import scala.{Any, Boolean, Int, IndexOutOfBoundsException} +import scala.{Any, Boolean, Int, IndexOutOfBoundsException, Option, Some, None} import strawman.collection.mutable.Iterator import strawman.collection.immutable.{List, Nil} @@ -9,22 +9,25 @@ import scala.annotation.unchecked.uncheckedVariance /** Base trait for sequence collections */ trait Seq[+A] extends Iterable[A] with SeqLike[A, Seq] with ArrayLike[A] +trait InhabitedSeq[+A] + extends Seq[A] + with InhabitedLinearSeqOps[A, Seq[A]] + /** Base trait for linearly accessed sequences that have efficient `head` and * `tail` operations. * Known subclasses: List, LazyList */ trait LinearSeq[+A] extends Seq[A] with LinearSeqLike[A, LinearSeq] { self => - /** To be overridden in implementations: */ - def isEmpty: Boolean - def head: A - def tail: LinearSeq[A] - /** `iterator` is overridden in terms of `head` and `tail` */ def iterator() = new Iterator[A] { - private[this] var current: Seq[A] = self + private[this] var current: LinearSeq[A] = self def hasNext = !current.isEmpty - def next() = { val r = current.head; current = current.tail; r } + def next() = { + val Some((head, tail)) = current.uncons + current = tail + head + } } /** `length` is defined in terms of `iterator` */ @@ -36,11 +39,23 @@ trait LinearSeq[+A] extends Seq[A] with LinearSeqLike[A, LinearSeq] { self => override def apply(n: Int): A = { if (n < 0) throw new IndexOutOfBoundsException(n.toString) val skipped = drop(n) - if (skipped.isEmpty) throw new IndexOutOfBoundsException(n.toString) - skipped.head + skipped.uncons.fold(throw new IndexOutOfBoundsException(n.toString))(_._1) } } +/** + * Collections that have at least one `head` element followed + * by a `tail`. + */ +trait InhabitedLinearSeqOps[+A, +Repr] extends Any { + + /** The first element of the collection. */ + def head: A + + /** The rest of the collection without its first element. */ + def tail: Repr +} + trait IndexedSeq[+A] extends Seq[A] { self => override def view: IndexedView[A] = new IndexedView[A] { def length: Int = self.length @@ -54,8 +69,17 @@ trait SeqLike[+A, +C[X] <: Seq[X]] extends IterableLike[A, C] with SeqMonoTransforms[A, C[A @uncheckedVariance]] // sound bcs of VarianceNote +trait InhabitedLinearSeqFactory[+C[X] <: InhabitedSeq[X]] { + def apply[A](a: A, as: A*): C[A] +} + /** Base trait for linear Seq operations */ -trait LinearSeqLike[+A, +C[X] <: LinearSeq[X]] extends SeqLike[A, C] { +trait LinearSeqLike[+A, +C[+X] <: LinearSeq[X]] extends SeqLike[A, C] { + + protected def coll: C[A] + + /** Extract the head and tail, if the collection is not empty */ + def uncons: Option[(A, C[A])] /** Optimized version of `drop` that avoids copying * Note: `drop` is defined here, rather than in a trait like `LinearSeqMonoTransforms`, @@ -63,15 +87,20 @@ trait LinearSeqLike[+A, +C[X] <: LinearSeq[X]] extends SeqLike[A, C] { * whereas we need to assume here that `Repr` is the same as the underlying * collection type. */ - override def drop(n: Int): C[A @uncheckedVariance] = { // sound bcs of VarianceNote - def loop(n: Int, s: Iterable[A]): C[A] = - if (n <= 0) s.asInstanceOf[C[A]] - // implicit contract to guarantee success of asInstanceOf: - // (1) coll is of type C[A] - // (2) The tail of a LinearSeq is of the same type as the type of the sequence itself - // it's surprisingly tricky/ugly to turn this into actual types, so we - // leave this contract implicit. - else loop(n - 1, s.tail) + override def drop(n: Int): C[A] = { + def loop(n: Int, s: C[A]): C[A] = { + // implicit contract to guarantee success of asInstanceOf: + // (1) coll is of type C[A] + // (2) The tail of a LinearSeq is of the same type as the type of the sequence itself + // it's surprisingly tricky/ugly to turn this into actual types, so we + // leave this contract implicit. + if (n <= 0) s else { + s.uncons match { + case None => s + case Some((_, t)) => loop(n - 1, t.asInstanceOf[C[A]]) + } + } + } loop(n, coll) } } diff --git a/src/main/scala/strawman/collection/immutable/LazyList.scala b/src/main/scala/strawman/collection/immutable/LazyList.scala index b9cf342948..998f28d9be 100644 --- a/src/main/scala/strawman/collection/immutable/LazyList.scala +++ b/src/main/scala/strawman/collection/immutable/LazyList.scala @@ -18,8 +18,6 @@ class LazyList[+A](expr: => LazyList.Evaluated[A]) } override def isEmpty = force.isEmpty - override def head = force.get._1 - override def tail = force.get._2 def #:: [B >: A](elem: => B): LazyList[B] = new LazyList(Some((elem, this))) @@ -34,6 +32,8 @@ class LazyList[+A](expr: => LazyList.Evaluated[A]) case Some((hd, tl)) => s"$hd #:: $tl" } else "LazyList(?)" + + def uncons: Option[(A, LazyList[A])] = force } object LazyList extends IterableFactory[LazyList] { diff --git a/src/main/scala/strawman/collection/immutable/List.scala b/src/main/scala/strawman/collection/immutable/List.scala index 623062dde8..c0f992d538 100644 --- a/src/main/scala/strawman/collection/immutable/List.scala +++ b/src/main/scala/strawman/collection/immutable/List.scala @@ -1,9 +1,9 @@ package strawman.collection.immutable import scala.annotation.unchecked.uncheckedVariance -import scala.Nothing +import scala.{Option, None, Nothing, Some} import scala.Predef.??? -import strawman.collection.{Iterable, IterableFactory, IterableOnce, LinearSeq, SeqLike} +import strawman.collection.{InhabitedLinearSeqFactory, InhabitedLinearSeqOps, InhabitedSeq, Iterable, IterableFactory, IterableOnce, LinearSeq, SeqLike, View} import strawman.collection.mutable.{Buildable, ListBuffer} @@ -18,12 +18,14 @@ sealed trait List[+A] protected[this] def newBuilder = new ListBuffer[A].mapResult(_.toList) /** Prepend element */ - def :: [B >: A](elem: B): List[B] = new ::(elem, this) + def :: [B >: A](elem: B): List.NonEmpty[B] = new ::(elem, this) /** Prepend operation that avoids copying this list */ def ++:[B >: A](prefix: List[B]): List[B] = - if (prefix.isEmpty) this - else prefix.head :: prefix.tail ++: this + prefix match { + case Nil => this + case h :: t => h :: (t ++: this) + } /** When concatenating with another list `xs`, avoid copying `xs` */ override def ++[B >: A](xs: IterableOnce[B]): List[B] = xs match { @@ -35,21 +37,30 @@ sealed trait List[+A] } case class :: [+A](x: A, private[collection] var next: List[A @uncheckedVariance]) // sound because `next` is used only locally - extends List[A] { + extends List[A] + with InhabitedSeq[A] + with InhabitedLinearSeqOps[A, List[A]] { override def isEmpty = false - override def head = x - override def tail = next + def head = x + def tail = next + def uncons: Option[(A, List[A])] = Some((x, next)) } case object Nil extends List[Nothing] { override def isEmpty = true - override def head = ??? - override def tail = ??? + def uncons = None } -object List extends IterableFactory[List] { +object List extends IterableFactory[List] with InhabitedLinearSeqFactory[::] { + + type NonEmpty[A] = ::[A] + def fromIterable[B](coll: Iterable[B]): List[B] = coll match { case coll: List[B] => coll case _ => ListBuffer.fromIterable(coll).toList } + + def apply[A](a: A, as: A*): List.NonEmpty[A] = + a :: fromIterable(View.Elems(as: _*)) + } \ No newline at end of file diff --git a/src/main/scala/strawman/collection/mutable/Iterator.scala b/src/main/scala/strawman/collection/mutable/Iterator.scala index 51ab5c7cba..a04dfa2aee 100644 --- a/src/main/scala/strawman/collection/mutable/Iterator.scala +++ b/src/main/scala/strawman/collection/mutable/Iterator.scala @@ -1,6 +1,6 @@ package strawman.collection.mutable -import scala.{Boolean, Int, Unit, Nothing, NoSuchElementException} +import scala.{Boolean, Int, Unit, Nothing, NoSuchElementException, Option, Some, None} import strawman.collection.{IndexedView, IterableOnce} /** A core Iterator class */ @@ -26,6 +26,9 @@ trait Iterator[+A] { self => while (hasNext) { len += 1; next() } len } + + def headOption: Option[A] = if (hasNext) Some(next()) else None + def filter(p: A => Boolean): Iterator[A] = new Iterator[A] { private var hd: A = _ private var hdDefined: Boolean = false diff --git a/src/test/scala/strawman/collection/test/Test.scala b/src/test/scala/strawman/collection/test/Test.scala index 5b7922a982..392868d864 100644 --- a/src/test/scala/strawman/collection/test/Test.scala +++ b/src/test/scala/strawman/collection/test/Test.scala @@ -11,7 +11,7 @@ import org.junit.Test class StrawmanTest { - def seqOps(xs: Seq[Int]): Unit = { + def seqOps(xs: InhabitedSeq[Int]): Unit = { val x1 = xs.foldLeft("")(_ + _) val y1: String = x1 val x2 = xs.foldRight("")(_ + _) @@ -69,8 +69,6 @@ class StrawmanTest { val y2: String = x2 val x3 = xs.indexWhere(_ % 2 == 0) val y3: Int = x3 - val x4 = xs.head - val y4: Int = x4 val x5 = xs.to(List) val y5: List[Int] = x5 val (xs6, xs7) = xs.partition(_ % 2 == 0) @@ -96,7 +94,6 @@ class StrawmanTest { println(x1) println(x2) println(x3) - println(x4) println(x5) println(xs6.to(List)) println(xs7.to(List)) @@ -117,8 +114,6 @@ class StrawmanTest { val y2: String = x2 val x3 = xs.indexWhere(_ % 2 == 0) val y3: Int = x3 - val x4 = xs.head - val y4: Int = x4 val x5 = xs.to(List) val y5: List[Char] = x5 val (xs6, xs7) = xs.partition(_ % 2 == 0) @@ -148,7 +143,6 @@ class StrawmanTest { println(x1) println(x2) println(x3) - println(x4) println(x5) println(xs6) println(xs7) @@ -171,8 +165,6 @@ class StrawmanTest { val y2: String = x2 val x3 = xs.indexWhere(_ % 2 == 0) val y3: Int = x3 - val x4 = xs.head - val y4: Int = x4 val x5 = xs.to(List) val y5: List[Int] = x5 val (xs6, xs7) = xs.partition(_ % 2 == 0) @@ -200,7 +192,6 @@ class StrawmanTest { println(x1) println(x2) println(x3) - println(x4) println(x5) println(xs6.view) println(xs7.view) @@ -222,8 +213,6 @@ class StrawmanTest { val y2: String = x2 val x3 = xs.indexWhere(_ % 2 == 0) val y3: Int = x3 - val x4 = xs.head - val y4: Int = x4 val x5 = xs.to(List) val y5: List[Int] = x5 val (xs6, xs7) = xs.partition(_ % 2 == 0) @@ -251,7 +240,6 @@ class StrawmanTest { println(x1) println(x2) println(x3) - println(x4) println(x5) println(xs6) println(xs6.to(List)) @@ -295,8 +283,8 @@ class StrawmanTest { val intsListBuf = ints.to(ListBuffer) val intsView = ints.view seqOps(ints) - seqOps(intsBuf) - seqOps(intsListBuf) +// seqOps(intsBuf) +// seqOps(intsListBuf) viewOps(intsView) stringOps("abc") arrayOps(Array(1, 2, 3)) From 0793e450da7ddc7d7f2d46041cd89dff77676d3b Mon Sep 17 00:00:00 2001 From: Julien Richard-Foy Date: Wed, 4 Jan 2017 16:05:11 +0100 Subject: [PATCH 2/2] Add unsafeHead and unsafeTail for optimizations --- src/main/scala/strawman/collection/Seq.scala | 21 ++++++++----------- .../strawman/collection/immutable/List.scala | 19 ++++++++++------- 2 files changed, 21 insertions(+), 19 deletions(-) diff --git a/src/main/scala/strawman/collection/Seq.scala b/src/main/scala/strawman/collection/Seq.scala index 9847271c6f..09b6cd78bc 100644 --- a/src/main/scala/strawman/collection/Seq.scala +++ b/src/main/scala/strawman/collection/Seq.scala @@ -23,11 +23,7 @@ trait LinearSeq[+A] extends Seq[A] with LinearSeqLike[A, LinearSeq] { self => def iterator() = new Iterator[A] { private[this] var current: LinearSeq[A] = self def hasNext = !current.isEmpty - def next() = { - val Some((head, tail)) = current.uncons - current = tail - head - } + def next() = { val r = current.unsafeHead; current = current.unsafeTail; r } } /** `length` is defined in terms of `iterator` */ @@ -39,7 +35,8 @@ trait LinearSeq[+A] extends Seq[A] with LinearSeqLike[A, LinearSeq] { self => override def apply(n: Int): A = { if (n < 0) throw new IndexOutOfBoundsException(n.toString) val skipped = drop(n) - skipped.uncons.fold(throw new IndexOutOfBoundsException(n.toString))(_._1) + if (skipped.isEmpty) throw new IndexOutOfBoundsException(n.toString) + skipped.unsafeHead } } @@ -81,6 +78,10 @@ trait LinearSeqLike[+A, +C[+X] <: LinearSeq[X]] extends SeqLike[A, C] { /** Extract the head and tail, if the collection is not empty */ def uncons: Option[(A, C[A])] + /** To be overriden for performance in subclasses */ + protected def unsafeHead: A = uncons.get._1 + protected def unsafeTail: C[A] = uncons.get._2 + /** Optimized version of `drop` that avoids copying * Note: `drop` is defined here, rather than in a trait like `LinearSeqMonoTransforms`, * because the `...MonoTransforms` traits make no assumption about the type of `Repr` @@ -89,17 +90,13 @@ trait LinearSeqLike[+A, +C[+X] <: LinearSeq[X]] extends SeqLike[A, C] { */ override def drop(n: Int): C[A] = { def loop(n: Int, s: C[A]): C[A] = { + if (n <= 0) s // implicit contract to guarantee success of asInstanceOf: // (1) coll is of type C[A] // (2) The tail of a LinearSeq is of the same type as the type of the sequence itself // it's surprisingly tricky/ugly to turn this into actual types, so we // leave this contract implicit. - if (n <= 0) s else { - s.uncons match { - case None => s - case Some((_, t)) => loop(n - 1, t.asInstanceOf[C[A]]) - } - } + else loop(n - 1, s.unsafeTail.asInstanceOf[C[A]]) } loop(n, coll) } diff --git a/src/main/scala/strawman/collection/immutable/List.scala b/src/main/scala/strawman/collection/immutable/List.scala index c0f992d538..56673facec 100644 --- a/src/main/scala/strawman/collection/immutable/List.scala +++ b/src/main/scala/strawman/collection/immutable/List.scala @@ -1,16 +1,16 @@ package strawman.collection.immutable import scala.annotation.unchecked.uncheckedVariance -import scala.{Option, None, Nothing, Some} +import scala.{Option, None, NoSuchElementException, Nothing, Some, UnsupportedOperationException} import scala.Predef.??? -import strawman.collection.{InhabitedLinearSeqFactory, InhabitedLinearSeqOps, InhabitedSeq, Iterable, IterableFactory, IterableOnce, LinearSeq, SeqLike, View} +import strawman.collection.{InhabitedLinearSeqFactory, InhabitedLinearSeqOps, InhabitedSeq, Iterable, IterableFactory, IterableOnce, LinearSeq, LinearSeqLike, View} import strawman.collection.mutable.{Buildable, ListBuffer} /** Concrete collection type: List */ sealed trait List[+A] extends LinearSeq[A] - with SeqLike[A, List] + with LinearSeqLike[A, List] with Buildable[A, List[A]] { def fromIterable[B](c: Iterable[B]): List[B] = List.fromIterable(c) @@ -22,10 +22,8 @@ sealed trait List[+A] /** Prepend operation that avoids copying this list */ def ++:[B >: A](prefix: List[B]): List[B] = - prefix match { - case Nil => this - case h :: t => h :: (t ++: this) - } + if (prefix.isEmpty) this + else prefix.unsafeHead :: (prefix.unsafeTail ++: this) /** When concatenating with another list `xs`, avoid copying `xs` */ override def ++[B >: A](xs: IterableOnce[B]): List[B] = xs match { @@ -40,15 +38,22 @@ case class :: [+A](x: A, private[collection] var next: List[A @uncheckedVariance extends List[A] with InhabitedSeq[A] with InhabitedLinearSeqOps[A, List[A]] { + override def isEmpty = false def head = x def tail = next + def uncons: Option[(A, List[A])] = Some((x, next)) + override protected def unsafeHead: A = x + override protected def unsafeTail: List[A] = next } case object Nil extends List[Nothing] { override def isEmpty = true + def uncons = None + override protected def unsafeHead: Nothing = throw new NoSuchElementException + override protected def unsafeTail: Nothing = throw new UnsupportedOperationException } object List extends IterableFactory[List] with InhabitedLinearSeqFactory[::] {