From 4bdf20841fda01751950f7fca4351eafa9f5f272 Mon Sep 17 00:00:00 2001 From: Luka Jacobowitz Date: Wed, 8 Aug 2018 13:56:38 +0200 Subject: [PATCH 01/19] Add Catenable --- core/src/main/scala/cats/data/Catenable.scala | 263 ++++++++++++++++++ 1 file changed, 263 insertions(+) create mode 100644 core/src/main/scala/cats/data/Catenable.scala diff --git a/core/src/main/scala/cats/data/Catenable.scala b/core/src/main/scala/cats/data/Catenable.scala new file mode 100644 index 0000000000..585925b337 --- /dev/null +++ b/core/src/main/scala/cats/data/Catenable.scala @@ -0,0 +1,263 @@ +package cats +package data + +import cats.implicits._ +import Catenable._ + +import scala.annotation.tailrec + +/** + * Trivial catenable sequence. Supports O(1) append, and (amortized) + * O(1) `uncons`, such that walking the sequence via N successive `uncons` + * steps takes O(N). Like a difference list, conversion to a `Seq[A]` + * takes linear time, regardless of how the sequence is built up. + */ +sealed abstract class Catenable[+A] { + + /** Returns the head and tail of this catenable if non empty, none otherwise. Amortized O(1). */ + final def uncons: Option[(A, Catenable[A])] = { + var c: Catenable[A] = this + val rights = new collection.mutable.ArrayBuffer[Catenable[A]] + var result: Option[(A, Catenable[A])] = null + while (result eq null) { + c match { + case Empty => + if (rights.isEmpty) { + result = None + } else { + c = rights.last + rights.trimEnd(1) + } + case Singleton(a) => + val next = + if (rights.isEmpty) empty + else rights.reduceLeft((x, y) => Append(y, x)) + result = Some(a -> next) + case Append(l, r) => c = l; rights += r + } + } + result + } + + /** Returns true if there are no elements in this collection. */ + def isEmpty: Boolean + + /** Returns false if there are no elements in this collection. */ + def nonEmpty: Boolean = !isEmpty + + /** Concatenates this with `c` in O(1) runtime. */ + final def ++[A2 >: A](c: Catenable[A2]): Catenable[A2] = + append(this, c) + + /** Returns a new catenable consisting of `a` followed by this. O(1) runtime. */ + final def cons[A2 >: A](a: A2): Catenable[A2] = + append(singleton(a), this) + + /** Alias for [[cons]]. */ + final def +:[A2 >: A](a: A2): Catenable[A2] = + cons(a) + + /** Returns a new catenable consisting of this followed by `a`. O(1) runtime. */ + final def snoc[A2 >: A](a: A2): Catenable[A2] = + append(this, singleton(a)) + + /** Alias for [[snoc]]. */ + final def :+[A2 >: A](a: A2): Catenable[A2] = + snoc(a) + + /** Applies the supplied function to each element and returns a new catenable. */ + final def map[B](f: A => B): Catenable[B] = + foldLeft(empty: Catenable[B])((acc, a) => acc :+ f(a)) + + /** Applies the supplied function to each element and returns a new catenable from the concatenated results */ + final def flatMap[B](f: A => Catenable[B]): Catenable[B] = + foldLeft(empty: Catenable[B])((acc, a) => acc ++ f(a)) + + /** Folds over the elements from left to right using the supplied initial value and function. */ + final def foldLeft[B](z: B)(f: (B, A) => B): B = { + var result = z + foreach(a => result = f(result, a)) + result + } + + /** collect `B` from this for which `f` is defined **/ + final def collect[B](f: PartialFunction[A, B]): Catenable[B] = { + val predicate = f.lift + foldLeft(Catenable.empty: Catenable[B]) { (acc, a) => + predicate(a).fold(acc)(b => acc :+ b) + } + } + + /** + * Yields to Some(a, Catenable[A]) with `a` removed where `f` holds for the first time, + * otherwise yields None, if `a` was not found + * Traverses only until `a` is found. + */ + final def deleteFirst(f: A => Boolean): Option[(A, Catenable[A])] = { + @tailrec + def go(rem: Catenable[A], acc: Catenable[A]): Option[(A, Catenable[A])] = + rem.uncons match { + case Some((a, tail)) => + if (!f(a)) go(tail, acc :+ a) + else Some((a, acc ++ tail)) + + case None => None + } + go(this, Catenable.empty) + } + + /** Applies the supplied function to each element, left to right. */ + private final def foreach(f: A => Unit): Unit = { + var c: Catenable[A] = this + val rights = new collection.mutable.ArrayBuffer[Catenable[A]] + while (c ne null) { + c match { + case Empty => + if (rights.isEmpty) { + c = null + } else { + c = rights.last + rights.trimEnd(1) + } + case Singleton(a) => + f(a) + c = + if (rights.isEmpty) Empty + else rights.reduceLeft((x, y) => Append(y, x)) + rights.clear() + case Append(l, r) => c = l; rights += r + } + } + } + + + /** Converts to a list. */ + final def toList: List[A] = { + val builder = List.newBuilder[A] + foreach { a => + builder += a; () + } + builder.result + } + + /** Converts to a vector. */ + final def toVector: Vector[A] = { + val builder = new scala.collection.immutable.VectorBuilder[A]() + foreach { a => + builder += a; () + } + builder.result + } + + def show[AA >: A](implicit AA: Show[AA]): String = { + val builder = new StringBuilder("Catenable(") + foreach { a => + builder ++= AA.show(a) + ", "; () + } + builder += ')' + builder.result + } + + override def toString = show(Show.show[A](_.toString)) +} + +object Catenable { + private[data] final case object Empty extends Catenable[Nothing] { + def isEmpty: Boolean = true + } + private[data] final case class Singleton[A](a: A) extends Catenable[A] { + def isEmpty: Boolean = false + } + private[data] final case class Append[A](left: Catenable[A], right: Catenable[A]) + extends Catenable[A] { + def isEmpty: Boolean = + false // b/c `append` constructor doesn't allow either branch to be empty + } + + /** Empty catenable. */ + val empty: Catenable[Nothing] = Empty + + /** Creates a catenable of 1 element. */ + def singleton[A](a: A): Catenable[A] = Singleton(a) + + /** Appends two catenables. */ + def append[A](c: Catenable[A], c2: Catenable[A]): Catenable[A] = + if (c.isEmpty) c2 + else if (c2.isEmpty) c + else Append(c, c2) + + /** Creates a catenable from the specified sequence. */ + def fromSeq[A](s: Seq[A]): Catenable[A] = + if (s.isEmpty) empty + else s.view.reverse.map(singleton).reduceLeft((x, y) => Append(y, x)) + + /** Creates a catenable from the specified elements. */ + def apply[A](as: A*): Catenable[A] = + as match { + case w: collection.mutable.WrappedArray[A] => + if (w.isEmpty) empty + else if (w.size == 1) singleton(w.head) + else { + val arr: Array[A] = w.array + var c: Catenable[A] = singleton(arr.last) + var idx = arr.size - 2 + while (idx >= 0) { + c = Append(singleton(arr(idx)), c) + idx -= 1 + } + c + } + case _ => fromSeq(as) + } +} + +private[data] sealed abstract class CatenableInstances { + implicit def catsDataMonoidForCatenable[A]: Monoid[Catenable[A]] = new Monoid[Catenable[A]] { + def empty: Catenable[A] = Catenable.empty + def combine(c: Catenable[A], c2: Catenable[A]): Catenable[A] = Catenable.append(c, c2) + } + + implicit val catsDataInstancesForCatenable: Traverse[Catenable] with Alternative[Catenable] with Monad[Catenable] = + new Traverse[Catenable] with Alternative[Catenable] with Monad[Catenable] { + def foldLeft[A, B](fa: Catenable[A], b: B)(f: (B, A) => B): B = + fa.foldLeft(b)(f) + def foldRight[A, B](fa: Catenable[A], b: Eval[B])(f: (A, Eval[B]) => Eval[B]): Eval[B] = + Foldable[List].foldRight(fa.toList, b)(f) + override def toList[A](fa: Catenable[A]): List[A] = fa.toList + override def isEmpty[A](fa: Catenable[A]): Boolean = fa.isEmpty + def traverse[F[_], A, B](fa: Catenable[A])(f: A => F[B])( + implicit G: Applicative[F]): F[Catenable[B]] = + Traverse[List].traverse(fa.toList)(f).map(Catenable.apply) + def empty[A]: Catenable[A] = Catenable.empty + def combineK[A](c: Catenable[A], c2: Catenable[A]): Catenable[A] = Catenable.append(c, c2) + def pure[A](a: A): Catenable[A] = Catenable.singleton(a) + def flatMap[A, B](fa: Catenable[A])(f: A => Catenable[B]): Catenable[B] = + fa.flatMap(f) + def tailRecM[A, B](a: A)(f: A => Catenable[Either[A, B]]): Catenable[B] = { + var acc: Catenable[B] = Catenable.empty + @tailrec def go(rest: List[Catenable[Either[A, B]]]): Unit = + rest match { + case hd :: tl => + hd.uncons match { + case Some((hdh, hdt)) => + hdh match { + case Right(b) => + acc = acc :+ b + go(hdt :: tl) + case Left(a) => + go(f(a) :: hdt :: tl) + } + case None => + go(tl) + } + case _ => () + } + go(f(a) :: Nil) + acc + } + } + + implicit def catsDataShowForCatenable[A](implicit A: Show[A]): Show[Catenable[A]] = + Show.show[Catenable[A]](_.show) + +} From 352b43353b29e1690cfc55c8c4b3750056613af0 Mon Sep 17 00:00:00 2001 From: Luka Jacobowitz Date: Wed, 8 Aug 2018 15:18:09 +0200 Subject: [PATCH 02/19] Add law tests and simple arbitrary and eq instances --- core/src/main/scala/cats/data/Catenable.scala | 23 ++++++++++++--- .../cats/laws/discipline/Arbitrary.scala | 11 +++++++ .../scala/cats/tests/CatenableSuite.scala | 29 +++++++++++++++++++ 3 files changed, 59 insertions(+), 4 deletions(-) create mode 100644 tests/src/test/scala/cats/tests/CatenableSuite.scala diff --git a/core/src/main/scala/cats/data/Catenable.scala b/core/src/main/scala/cats/data/Catenable.scala index 585925b337..43ca4f796f 100644 --- a/core/src/main/scala/cats/data/Catenable.scala +++ b/core/src/main/scala/cats/data/Catenable.scala @@ -18,6 +18,7 @@ sealed abstract class Catenable[+A] { final def uncons: Option[(A, Catenable[A])] = { var c: Catenable[A] = this val rights = new collection.mutable.ArrayBuffer[Catenable[A]] + // scalastyle:off null var result: Option[(A, Catenable[A])] = null while (result eq null) { c match { @@ -36,6 +37,7 @@ sealed abstract class Catenable[+A] { case Append(l, r) => c = l; rights += r } } + // scalastyle:on null result } @@ -80,7 +82,7 @@ sealed abstract class Catenable[+A] { result } - /** collect `B` from this for which `f` is defined **/ + /** Collect `B` from this for which `f` is defined */ final def collect[B](f: PartialFunction[A, B]): Catenable[B] = { val predicate = f.lift foldLeft(Catenable.empty: Catenable[B]) { (acc, a) => @@ -110,6 +112,7 @@ sealed abstract class Catenable[+A] { private final def foreach(f: A => Unit): Unit = { var c: Catenable[A] = this val rights = new collection.mutable.ArrayBuffer[Catenable[A]] + // scalastyle:off null while (c ne null) { c match { case Empty => @@ -128,6 +131,7 @@ sealed abstract class Catenable[+A] { case Append(l, r) => c = l; rights += r } } + // scalastyle:on null } @@ -151,17 +155,21 @@ sealed abstract class Catenable[+A] { def show[AA >: A](implicit AA: Show[AA]): String = { val builder = new StringBuilder("Catenable(") + var first = true + foreach { a => - builder ++= AA.show(a) + ", "; () + if (first) { builder ++= AA.show(a); first = false } + else builder ++= ", " + AA.show(a) + () } builder += ')' builder.result } - override def toString = show(Show.show[A](_.toString)) + override def toString: String = show(Show.show[A](_.toString)) } -object Catenable { +object Catenable extends CatenableInstances { private[data] final case object Empty extends Catenable[Nothing] { def isEmpty: Boolean = true } @@ -223,6 +231,8 @@ private[data] sealed abstract class CatenableInstances { fa.foldLeft(b)(f) def foldRight[A, B](fa: Catenable[A], b: Eval[B])(f: (A, Eval[B]) => Eval[B]): Eval[B] = Foldable[List].foldRight(fa.toList, b)(f) + + override def map[A, B](fa: Catenable[A])(f: A => B): Catenable[B] = fa.map(f) override def toList[A](fa: Catenable[A]): List[A] = fa.toList override def isEmpty[A](fa: Catenable[A]): Boolean = fa.isEmpty def traverse[F[_], A, B](fa: Catenable[A])(f: A => F[B])( @@ -260,4 +270,9 @@ private[data] sealed abstract class CatenableInstances { implicit def catsDataShowForCatenable[A](implicit A: Show[A]): Show[Catenable[A]] = Show.show[Catenable[A]](_.show) + implicit def catsDataEqForCatenable[A](implicit A: Eq[A]): Eq[Catenable[A]] = new Eq[Catenable[A]] { + def eqv(x: Catenable[A], y: Catenable[A]): Boolean = + (x eq y) || x.toList === y.toList + } + } diff --git a/laws/src/main/scala/cats/laws/discipline/Arbitrary.scala b/laws/src/main/scala/cats/laws/discipline/Arbitrary.scala index c7d077dfd6..566d4f8fb2 100644 --- a/laws/src/main/scala/cats/laws/discipline/Arbitrary.scala +++ b/laws/src/main/scala/cats/laws/discipline/Arbitrary.scala @@ -276,6 +276,17 @@ object arbitrary extends ArbitraryInstances0 { implicit def catsLawsCogenForAndThen[A, B](implicit F: Cogen[A => B]): Cogen[AndThen[A, B]] = Cogen((seed, x) => F.perturb(seed, x)) + implicit def catsLawsArbitraryForCatenable[A](implicit A: Arbitrary[A]): Arbitrary[Catenable[A]] = + Arbitrary(Gen.sized { + case 0 => Gen.const(Catenable.empty) + case 1 => A.arbitrary.map(Catenable.singleton) + case 2 => A.arbitrary.flatMap(a1 => A.arbitrary.flatMap(a2 => + Catenable.append(Catenable.singleton(a1), Catenable.singleton(a2)))) + case n => Catenable.fromSeq(Range.apply(0, n)).foldLeft(Gen.const(Catenable.empty: Catenable[A])) { (gen, _) => + gen.flatMap(cat => A.arbitrary.map(a => cat :+ a)) + } + }) + } private[discipline] sealed trait ArbitraryInstances0 { diff --git a/tests/src/test/scala/cats/tests/CatenableSuite.scala b/tests/src/test/scala/cats/tests/CatenableSuite.scala new file mode 100644 index 0000000000..8364b8dd44 --- /dev/null +++ b/tests/src/test/scala/cats/tests/CatenableSuite.scala @@ -0,0 +1,29 @@ +package cats +package tests + +import cats.data.Catenable +import cats.kernel.laws.discipline.MonoidTests +import cats.laws.discipline.{AlternativeTests, MonadTests, SerializableTests, TraverseTests} +import cats.laws.discipline.arbitrary._ + +class CatenableSuite extends CatsSuite { + checkAll("Catenable[Int]", AlternativeTests[Catenable].alternative[Int, Int, Int]) + checkAll("Alternative[Catenable]", SerializableTests.serializable(Alternative[Catenable])) + + checkAll("Catenable[Int] with Option", TraverseTests[Catenable].traverse[Int, Int, Int, Set[Int], Option, Option]) + checkAll("Traverse[Catenable]", SerializableTests.serializable(Traverse[Catenable])) + + checkAll("Catenable[Int]", MonadTests[Catenable].monad[Int, Int, Int]) + checkAll("Monad[Catenable]", SerializableTests.serializable(Monad[Catenable])) + + checkAll("Catenable[Int]", MonoidTests[Catenable[Int]].monoid) + checkAll("Monoid[Catenable]", SerializableTests.serializable(Monoid[Catenable[Int]])) + + test("show"){ + Show[Catenable[Int]].show(Catenable(1, 2, 3)) should === ("Catenable(1, 2, 3)") + (Catenable.empty: Catenable[Int]).show should === ("Catenable()") + forAll { l: Catenable[String] => + l.show should === (l.toString) + } + } +} From 68544a6986ace14cdb373045b31894259719dab8 Mon Sep 17 00:00:00 2001 From: Luka Jacobowitz Date: Wed, 8 Aug 2018 18:26:49 +0200 Subject: [PATCH 03/19] More tests --- core/src/main/scala/cats/data/Catenable.scala | 65 +++++++++++++++++-- .../scala/cats/tests/CatenableSuite.scala | 30 +++++++++ 2 files changed, 90 insertions(+), 5 deletions(-) diff --git a/core/src/main/scala/cats/data/Catenable.scala b/core/src/main/scala/cats/data/Catenable.scala index 43ca4f796f..0447516ec5 100644 --- a/core/src/main/scala/cats/data/Catenable.scala +++ b/core/src/main/scala/cats/data/Catenable.scala @@ -83,13 +83,50 @@ sealed abstract class Catenable[+A] { } /** Collect `B` from this for which `f` is defined */ - final def collect[B](f: PartialFunction[A, B]): Catenable[B] = { - val predicate = f.lift + final def collect[B](pf: PartialFunction[A, B]): Catenable[B] = foldLeft(Catenable.empty: Catenable[B]) { (acc, a) => - predicate(a).fold(acc)(b => acc :+ b) + // trick from TraversableOnce, used to avoid calling both isDefined and apply (or calling lift) + val x = pf.applyOrElse(a, sentinel) + if (x.asInstanceOf[AnyRef] ne sentinel) acc :+ x.asInstanceOf[B] + else acc } + + /** Remove elements not matching the predicate */ + final def filter(f: A => Boolean): Catenable[A] = + collect { case a if f(a) => a } + + /** Remove elements matching the predicate */ + final def filterNot(f: A => Boolean): Catenable[A] = + filter(a => !f(a)) + + /** Find the first element matching the predicate, if one exists */ + final def find(f: A => Boolean): Option[A] = { + var result: Option[A] = Option.empty[A] + foreachUntil { a => + val b = f(a) + if (b) result = Option(a) + b + } + result + } + + /** Check whether at least one element satisfies the predicate */ + def exists(f: A => Boolean): Boolean = { + var result: Boolean = false + foreachUntil { a => + val b = f(a) + if (b) result = true + b + } + result } + /** Check whether all elements satisfy the predicate */ + def forall(f: A => Boolean): Boolean = + exists(a => !f(a)) + + + /** * Yields to Some(a, Catenable[A]) with `a` removed where `f` holds for the first time, * otherwise yields None, if `a` was not found @@ -109,7 +146,11 @@ sealed abstract class Catenable[+A] { } /** Applies the supplied function to each element, left to right. */ - private final def foreach(f: A => Unit): Unit = { + private final def foreach(f: A => Unit): Unit = + foreachUntil { a => f(a); false } + + /** Applies the supplied function to each element, left to right, but stops when true is returned */ + private final def foreachUntil(f: A => Boolean): Unit = { var c: Catenable[A] = this val rights = new collection.mutable.ArrayBuffer[Catenable[A]] // scalastyle:off null @@ -123,7 +164,8 @@ sealed abstract class Catenable[+A] { rights.trimEnd(1) } case Singleton(a) => - f(a) + val b = f(a) + if (b) return (); c = if (rights.isEmpty) Empty else rights.reduceLeft((x, y) => Append(y, x)) @@ -134,6 +176,16 @@ sealed abstract class Catenable[+A] { // scalastyle:on null } + /** Returns the number of elements in this structure */ + final def length: Int = { + var i: Int = 0 + foreach(_ => i += 1) + i + } + + /** Alias for length */ + final def size: Int = length + /** Converts to a list. */ final def toList: List[A] = { @@ -170,6 +222,9 @@ sealed abstract class Catenable[+A] { } object Catenable extends CatenableInstances { + + private val sentinel: Function1[Any, Any] = new scala.runtime.AbstractFunction1[Any, Any]{ def apply(a: Any) = this } + private[data] final case object Empty extends Catenable[Nothing] { def isEmpty: Boolean = true } diff --git a/tests/src/test/scala/cats/tests/CatenableSuite.scala b/tests/src/test/scala/cats/tests/CatenableSuite.scala index 8364b8dd44..0aa728c4de 100644 --- a/tests/src/test/scala/cats/tests/CatenableSuite.scala +++ b/tests/src/test/scala/cats/tests/CatenableSuite.scala @@ -26,4 +26,34 @@ class CatenableSuite extends CatsSuite { l.show should === (l.toString) } } + + test("size is consistent with toList.size") { + forAll { (ci: Catenable[Int]) => + ci.size should === (ci.toList.size) + } + } + + test("filterNot and then forall should always be false") { + forAll { (ci: Catenable[Int], f: Int => Boolean) => + ci.filterNot(f).exists(f) === false + } + } + + test("exists should be consistent with find + isDefined") { + forAll { (ci: Catenable[Int], f: Int => Boolean) => + ci.exists(f) === ci.find(f).isDefined + } + } + + test("Always nonempty after cons") { + forAll { (ci: Catenable[Int], i: Int) => + ci.cons(i).nonEmpty === true + } + } + + test("fromSeq . toVector is id") { + forAll { (ci: Catenable[Int]) => + Catenable.fromSeq(ci.toVector) === ci + } + } } From 17c10ad1600f4e5fd680c8e8260c0fab8745f7af Mon Sep 17 00:00:00 2001 From: Luka Jacobowitz Date: Wed, 8 Aug 2018 19:07:59 +0200 Subject: [PATCH 04/19] Add benchmarks --- .../scala/cats/bench/CatenableBench.scala | 54 +++++++++++++++++++ core/src/main/scala/cats/data/Catenable.scala | 30 ++++++----- .../cats/laws/discipline/Arbitrary.scala | 4 +- .../scala/cats/tests/CatenableSuite.scala | 2 +- 4 files changed, 74 insertions(+), 16 deletions(-) create mode 100644 bench/src/main/scala/cats/bench/CatenableBench.scala diff --git a/bench/src/main/scala/cats/bench/CatenableBench.scala b/bench/src/main/scala/cats/bench/CatenableBench.scala new file mode 100644 index 0000000000..eacfb1af88 --- /dev/null +++ b/bench/src/main/scala/cats/bench/CatenableBench.scala @@ -0,0 +1,54 @@ +package cats.bench + +import cats.data.Catenable +import cats.implicits._ + +import org.openjdk.jmh.annotations.{Benchmark, Scope, State} + +@State(Scope.Thread) +class CatenableBenchmark { + + private val smallCatenable = Catenable(1, 2, 3, 4, 5) + private val smallVector = Vector(1, 2, 3, 4, 5) + private val smallList = List(1, 2, 3, 4, 5) + + private val largeCatenable = Catenable.fromSeq(0 to 1000000) + private val largeVector = (0 to 1000000).toVector + private val largeList = (0 to 1000000).toList + + private val largeNestedCatenable = largeList.map(Catenable.singleton) + private val largeNestedVector = largeList.map(Vector(_)) + private val largeNestedList = largeList.map(List(_)) + + @Benchmark def mapSmallCatenable: Catenable[Int] = smallCatenable.map(_ + 1) + @Benchmark def mapSmallVector: Vector[Int] = smallVector.map(_ + 1) + @Benchmark def mapSmallList: List[Int] = smallList.map(_ + 1) + @Benchmark def mapLargeCatenable: Catenable[Int] = largeCatenable.map(_ + 1) + @Benchmark def mapLargeVector: Vector[Int] = largeVector.map(_ + 1) + @Benchmark def mapLargeList: List[Int] = largeList.map(_ + 1) + + @Benchmark def foldLeftSmallCatenable: Int = smallCatenable.foldLeft(0)(_ + _) + @Benchmark def foldLeftSmallVector: Int = smallVector.foldLeft(0)(_ + _) + @Benchmark def foldLeftSmallList: Int = smallList.foldLeft(0)(_ + _) + @Benchmark def foldLeftLargeCatenable: Int = largeCatenable.foldLeft(0)(_ + _) + @Benchmark def foldLeftLargeVector: Int = largeVector.foldLeft(0)(_ + _) + @Benchmark def foldLeftLargeList: Int = largeList.foldLeft(0)(_ + _) + + @Benchmark def consSmallCatenable: Catenable[Int] = 0 +: smallCatenable + @Benchmark def consSmallVector: Vector[Int] = 0 +: smallVector + @Benchmark def consSmallList: List[Int] = 0 +: smallList + @Benchmark def consLargeCatenable: Catenable[Int] = 0 +: largeCatenable + @Benchmark def consLargeVector: Vector[Int] = 0 +: largeVector + @Benchmark def consLargeList: List[Int] = 0 +: largeList + + @Benchmark def createTinyCatenable: Catenable[Int] = Catenable(1) + @Benchmark def createTinyVector: Vector[Int] = Vector(1) + @Benchmark def createTinyList: List[Int] = List(1) + @Benchmark def createSmallCatenable: Catenable[Int] = Catenable(1, 2, 3, 4, 5) + @Benchmark def createSmallVector: Vector[Int] = Vector(1, 2, 3, 4, 5) + @Benchmark def createSmallList: List[Int] = List(1, 2, 3, 4, 5) + + @Benchmark def accumulateCatenable: Catenable[Int] = largeList.foldMap(Catenable.singleton) + @Benchmark def accumulateVector: Vector[Int] = largeList.foldMap(Vector(_)) + @Benchmark def accumulateList: List[Int] = largeList.foldMap(List(_)) +} diff --git a/core/src/main/scala/cats/data/Catenable.scala b/core/src/main/scala/cats/data/Catenable.scala index 0447516ec5..c2fb7bad00 100644 --- a/core/src/main/scala/cats/data/Catenable.scala +++ b/core/src/main/scala/cats/data/Catenable.scala @@ -31,7 +31,7 @@ sealed abstract class Catenable[+A] { } case Singleton(a) => val next = - if (rights.isEmpty) empty + if (rights.isEmpty) nil else rights.reduceLeft((x, y) => Append(y, x)) result = Some(a -> next) case Append(l, r) => c = l; rights += r @@ -69,11 +69,11 @@ sealed abstract class Catenable[+A] { /** Applies the supplied function to each element and returns a new catenable. */ final def map[B](f: A => B): Catenable[B] = - foldLeft(empty: Catenable[B])((acc, a) => acc :+ f(a)) + foldLeft(nil: Catenable[B])((acc, a) => acc :+ f(a)) /** Applies the supplied function to each element and returns a new catenable from the concatenated results */ final def flatMap[B](f: A => Catenable[B]): Catenable[B] = - foldLeft(empty: Catenable[B])((acc, a) => acc ++ f(a)) + foldLeft(nil: Catenable[B])((acc, a) => acc ++ f(a)) /** Folds over the elements from left to right using the supplied initial value and function. */ final def foldLeft[B](z: B)(f: (B, A) => B): B = { @@ -84,7 +84,7 @@ sealed abstract class Catenable[+A] { /** Collect `B` from this for which `f` is defined */ final def collect[B](pf: PartialFunction[A, B]): Catenable[B] = - foldLeft(Catenable.empty: Catenable[B]) { (acc, a) => + foldLeft(Catenable.nil: Catenable[B]) { (acc, a) => // trick from TraversableOnce, used to avoid calling both isDefined and apply (or calling lift) val x = pf.applyOrElse(a, sentinel) if (x.asInstanceOf[AnyRef] ne sentinel) acc :+ x.asInstanceOf[B] @@ -111,7 +111,7 @@ sealed abstract class Catenable[+A] { } /** Check whether at least one element satisfies the predicate */ - def exists(f: A => Boolean): Boolean = { + final def exists(f: A => Boolean): Boolean = { var result: Boolean = false foreachUntil { a => val b = f(a) @@ -122,9 +122,11 @@ sealed abstract class Catenable[+A] { } /** Check whether all elements satisfy the predicate */ - def forall(f: A => Boolean): Boolean = + final def forall(f: A => Boolean): Boolean = exists(a => !f(a)) + final def contains[AA >: A](a: AA)(implicit A: Eq[AA]): Boolean = + exists(A.eqv(a, _)) /** @@ -142,7 +144,7 @@ sealed abstract class Catenable[+A] { case None => None } - go(this, Catenable.empty) + go(this, Catenable.nil) } /** Applies the supplied function to each element, left to right. */ @@ -238,7 +240,9 @@ object Catenable extends CatenableInstances { } /** Empty catenable. */ - val empty: Catenable[Nothing] = Empty + val nil: Catenable[Nothing] = Empty + + def empty[A]: Catenable[A] = nil /** Creates a catenable of 1 element. */ def singleton[A](a: A): Catenable[A] = Singleton(a) @@ -251,14 +255,14 @@ object Catenable extends CatenableInstances { /** Creates a catenable from the specified sequence. */ def fromSeq[A](s: Seq[A]): Catenable[A] = - if (s.isEmpty) empty + if (s.isEmpty) nil else s.view.reverse.map(singleton).reduceLeft((x, y) => Append(y, x)) /** Creates a catenable from the specified elements. */ def apply[A](as: A*): Catenable[A] = as match { case w: collection.mutable.WrappedArray[A] => - if (w.isEmpty) empty + if (w.isEmpty) nil else if (w.size == 1) singleton(w.head) else { val arr: Array[A] = w.array @@ -276,7 +280,7 @@ object Catenable extends CatenableInstances { private[data] sealed abstract class CatenableInstances { implicit def catsDataMonoidForCatenable[A]: Monoid[Catenable[A]] = new Monoid[Catenable[A]] { - def empty: Catenable[A] = Catenable.empty + def empty: Catenable[A] = Catenable.nil def combine(c: Catenable[A], c2: Catenable[A]): Catenable[A] = Catenable.append(c, c2) } @@ -293,13 +297,13 @@ private[data] sealed abstract class CatenableInstances { def traverse[F[_], A, B](fa: Catenable[A])(f: A => F[B])( implicit G: Applicative[F]): F[Catenable[B]] = Traverse[List].traverse(fa.toList)(f).map(Catenable.apply) - def empty[A]: Catenable[A] = Catenable.empty + def empty[A]: Catenable[A] = Catenable.nil def combineK[A](c: Catenable[A], c2: Catenable[A]): Catenable[A] = Catenable.append(c, c2) def pure[A](a: A): Catenable[A] = Catenable.singleton(a) def flatMap[A, B](fa: Catenable[A])(f: A => Catenable[B]): Catenable[B] = fa.flatMap(f) def tailRecM[A, B](a: A)(f: A => Catenable[Either[A, B]]): Catenable[B] = { - var acc: Catenable[B] = Catenable.empty + var acc: Catenable[B] = Catenable.nil @tailrec def go(rest: List[Catenable[Either[A, B]]]): Unit = rest match { case hd :: tl => diff --git a/laws/src/main/scala/cats/laws/discipline/Arbitrary.scala b/laws/src/main/scala/cats/laws/discipline/Arbitrary.scala index 566d4f8fb2..130966a9fd 100644 --- a/laws/src/main/scala/cats/laws/discipline/Arbitrary.scala +++ b/laws/src/main/scala/cats/laws/discipline/Arbitrary.scala @@ -278,11 +278,11 @@ object arbitrary extends ArbitraryInstances0 { implicit def catsLawsArbitraryForCatenable[A](implicit A: Arbitrary[A]): Arbitrary[Catenable[A]] = Arbitrary(Gen.sized { - case 0 => Gen.const(Catenable.empty) + case 0 => Gen.const(Catenable.nil) case 1 => A.arbitrary.map(Catenable.singleton) case 2 => A.arbitrary.flatMap(a1 => A.arbitrary.flatMap(a2 => Catenable.append(Catenable.singleton(a1), Catenable.singleton(a2)))) - case n => Catenable.fromSeq(Range.apply(0, n)).foldLeft(Gen.const(Catenable.empty: Catenable[A])) { (gen, _) => + case n => Catenable.fromSeq(Range.apply(0, n)).foldLeft(Gen.const(Catenable.empty[A])) { (gen, _) => gen.flatMap(cat => A.arbitrary.map(a => cat :+ a)) } }) diff --git a/tests/src/test/scala/cats/tests/CatenableSuite.scala b/tests/src/test/scala/cats/tests/CatenableSuite.scala index 0aa728c4de..45cf940a23 100644 --- a/tests/src/test/scala/cats/tests/CatenableSuite.scala +++ b/tests/src/test/scala/cats/tests/CatenableSuite.scala @@ -21,7 +21,7 @@ class CatenableSuite extends CatsSuite { test("show"){ Show[Catenable[Int]].show(Catenable(1, 2, 3)) should === ("Catenable(1, 2, 3)") - (Catenable.empty: Catenable[Int]).show should === ("Catenable()") + Catenable.empty[Int].show should === ("Catenable()") forAll { l: Catenable[String] => l.show should === (l.toString) } From 0c4cc5d53e5aafc986450d5003c4628905e9865d Mon Sep 17 00:00:00 2001 From: Luka Jacobowitz Date: Wed, 8 Aug 2018 23:22:45 +0200 Subject: [PATCH 05/19] Add chain benchmarks --- .../scala/cats/bench/CatenableBench.scala | 6 ----- .../cats/bench/CollectionMonoidBench.scala | 24 +++++++++++++++++++ build.sbt | 4 +++- 3 files changed, 27 insertions(+), 7 deletions(-) create mode 100644 bench/src/main/scala/cats/bench/CollectionMonoidBench.scala diff --git a/bench/src/main/scala/cats/bench/CatenableBench.scala b/bench/src/main/scala/cats/bench/CatenableBench.scala index eacfb1af88..08a0a854f5 100644 --- a/bench/src/main/scala/cats/bench/CatenableBench.scala +++ b/bench/src/main/scala/cats/bench/CatenableBench.scala @@ -1,8 +1,6 @@ package cats.bench import cats.data.Catenable -import cats.implicits._ - import org.openjdk.jmh.annotations.{Benchmark, Scope, State} @State(Scope.Thread) @@ -47,8 +45,4 @@ class CatenableBenchmark { @Benchmark def createSmallCatenable: Catenable[Int] = Catenable(1, 2, 3, 4, 5) @Benchmark def createSmallVector: Vector[Int] = Vector(1, 2, 3, 4, 5) @Benchmark def createSmallList: List[Int] = List(1, 2, 3, 4, 5) - - @Benchmark def accumulateCatenable: Catenable[Int] = largeList.foldMap(Catenable.singleton) - @Benchmark def accumulateVector: Vector[Int] = largeList.foldMap(Vector(_)) - @Benchmark def accumulateList: List[Int] = largeList.foldMap(List(_)) } diff --git a/bench/src/main/scala/cats/bench/CollectionMonoidBench.scala b/bench/src/main/scala/cats/bench/CollectionMonoidBench.scala new file mode 100644 index 0000000000..4dfa6714d2 --- /dev/null +++ b/bench/src/main/scala/cats/bench/CollectionMonoidBench.scala @@ -0,0 +1,24 @@ +package cats.bench + +import cats.Monoid +import cats.data.Catenable +import cats.implicits._ +import chain.Chain +import org.openjdk.jmh.annotations.{Benchmark, Scope, State} + +@State(Scope.Thread) +class CollectionMonoidBench { + + private val largeList = (0 to 1000000).toList + + implicit def monoidChain[A]: Monoid[Chain[A]] = new Monoid[Chain[A]] { + def empty: Chain[A] = Chain.empty[A] + + def combine(x: Chain[A], y: Chain[A]): Chain[A] = x ++ y + } + + @Benchmark def accumulateCatenable: Catenable[Int] = largeList.foldMap(Catenable.singleton) + @Benchmark def accumulateVector: Vector[Int] = largeList.foldMap(Vector(_)) + @Benchmark def accumulateList: List[Int] = largeList.foldMap(List(_)) + @Benchmark def accumulateChain: Chain[Int] = largeList.foldMap(Chain.single) +} diff --git a/build.sbt b/build.sbt index 588726cc24..38211c288e 100644 --- a/build.sbt +++ b/build.sbt @@ -491,7 +491,9 @@ lazy val bench = project.dependsOn(macrosJVM, coreJVM, freeJVM, lawsJVM) .settings(commonJvmSettings) .settings(coverageEnabled := false) .settings(libraryDependencies ++= Seq( - "org.scalaz" %% "scalaz-core" % "7.2.23")) + "org.scalaz" %% "scalaz-core" % "7.2.23", + "org.spire-math" %% "chain" % "0.3.0" + )) .enablePlugins(JmhPlugin) // cats-js is JS-only From 2854651e088db0762b37dc262f9c323c66d92c7a Mon Sep 17 00:00:00 2001 From: Luka Jacobowitz Date: Thu, 9 Aug 2018 10:43:09 +0200 Subject: [PATCH 06/19] Add Iterator --- .../scala/cats/bench/CatenableBench.scala | 2 +- core/src/main/scala/cats/data/Catenable.scala | 35 ++++++++++++++++--- .../scala/cats/tests/CatenableSuite.scala | 6 ++++ 3 files changed, 38 insertions(+), 5 deletions(-) diff --git a/bench/src/main/scala/cats/bench/CatenableBench.scala b/bench/src/main/scala/cats/bench/CatenableBench.scala index 08a0a854f5..7d543b4518 100644 --- a/bench/src/main/scala/cats/bench/CatenableBench.scala +++ b/bench/src/main/scala/cats/bench/CatenableBench.scala @@ -4,7 +4,7 @@ import cats.data.Catenable import org.openjdk.jmh.annotations.{Benchmark, Scope, State} @State(Scope.Thread) -class CatenableBenchmark { +class CatenableBench { private val smallCatenable = Catenable(1, 2, 3, 4, 5) private val smallVector = Vector(1, 2, 3, 4, 5) diff --git a/core/src/main/scala/cats/data/Catenable.scala b/core/src/main/scala/cats/data/Catenable.scala index c2fb7bad00..d865123a07 100644 --- a/core/src/main/scala/cats/data/Catenable.scala +++ b/core/src/main/scala/cats/data/Catenable.scala @@ -148,14 +148,13 @@ sealed abstract class Catenable[+A] { } /** Applies the supplied function to each element, left to right. */ - private final def foreach(f: A => Unit): Unit = - foreachUntil { a => f(a); false } + private final def foreach(f: A => Unit): Unit = foreachUntil { a => f(a); false } /** Applies the supplied function to each element, left to right, but stops when true is returned */ private final def foreachUntil(f: A => Boolean): Unit = { var c: Catenable[A] = this val rights = new collection.mutable.ArrayBuffer[Catenable[A]] - // scalastyle:off null + // scalastyle:off null return while (c ne null) { c match { case Empty => @@ -175,9 +174,12 @@ sealed abstract class Catenable[+A] { case Append(l, r) => c = l; rights += r } } - // scalastyle:on null + // scalastyle:on null return } + + final def iterator: Iterator[A] = new CatenableIterator[A](this) + /** Returns the number of elements in this structure */ final def length: Int = { var i: Int = 0 @@ -276,6 +278,31 @@ object Catenable extends CatenableInstances { } case _ => fromSeq(as) } + + class CatenableIterator[A](self: Catenable[A]) extends Iterator[A] { + var c: Catenable[A] = if (self.isEmpty) null else self + val rights = new collection.mutable.ArrayBuffer[Catenable[A]] + + override def hasNext: Boolean = c ne null + + override def next(): A = { + @tailrec def go: A = c match { + case Empty => + go // This can't happen + case Singleton(a) => + c = + if (rights.isEmpty) null + else rights.reduceLeft((x, y) => Append(y, x)) + rights.clear() + a + case Append(l, r) => + c = l + rights += r + go + } + go + } + } } private[data] sealed abstract class CatenableInstances { diff --git a/tests/src/test/scala/cats/tests/CatenableSuite.scala b/tests/src/test/scala/cats/tests/CatenableSuite.scala index 45cf940a23..06a994e676 100644 --- a/tests/src/test/scala/cats/tests/CatenableSuite.scala +++ b/tests/src/test/scala/cats/tests/CatenableSuite.scala @@ -56,4 +56,10 @@ class CatenableSuite extends CatsSuite { Catenable.fromSeq(ci.toVector) === ci } } + + test("fromSeq . toList . iterator is id") { + forAll { (ci: Catenable[Int]) => + Catenable.fromSeq(ci.iterator.toList) === ci + } + } } From af5cf2bee55fcc84d05b2f9cd999beaa39ca9e63 Mon Sep 17 00:00:00 2001 From: Luka Jacobowitz Date: Thu, 9 Aug 2018 11:31:44 +0200 Subject: [PATCH 07/19] More chain benchmarks --- .../scala/cats/bench/CatenableBench.scala | 21 ++++++++++++++++--- core/src/main/scala/cats/data/Catenable.scala | 5 +++++ .../cats/laws/discipline/Arbitrary.scala | 4 ++-- 3 files changed, 25 insertions(+), 5 deletions(-) diff --git a/bench/src/main/scala/cats/bench/CatenableBench.scala b/bench/src/main/scala/cats/bench/CatenableBench.scala index 7d543b4518..a5a2793580 100644 --- a/bench/src/main/scala/cats/bench/CatenableBench.scala +++ b/bench/src/main/scala/cats/bench/CatenableBench.scala @@ -1,6 +1,7 @@ package cats.bench import cats.data.Catenable +import chain.Chain import org.openjdk.jmh.annotations.{Benchmark, Scope, State} @State(Scope.Thread) @@ -9,40 +10,54 @@ class CatenableBench { private val smallCatenable = Catenable(1, 2, 3, 4, 5) private val smallVector = Vector(1, 2, 3, 4, 5) private val smallList = List(1, 2, 3, 4, 5) + private val smallChain = Chain(smallList) private val largeCatenable = Catenable.fromSeq(0 to 1000000) private val largeVector = (0 to 1000000).toVector private val largeList = (0 to 1000000).toList + private val largeChain = (0 to 1000).foldLeft(Chain.empty[Int])((acc, _) => acc ++ Chain(0 to 1000)) - private val largeNestedCatenable = largeList.map(Catenable.singleton) - private val largeNestedVector = largeList.map(Vector(_)) - private val largeNestedList = largeList.map(List(_)) @Benchmark def mapSmallCatenable: Catenable[Int] = smallCatenable.map(_ + 1) @Benchmark def mapSmallVector: Vector[Int] = smallVector.map(_ + 1) @Benchmark def mapSmallList: List[Int] = smallList.map(_ + 1) + @Benchmark def mapSmallChain: Chain[Int] = smallChain.map(_ + 1) + @Benchmark def mapLargeCatenable: Catenable[Int] = largeCatenable.map(_ + 1) @Benchmark def mapLargeVector: Vector[Int] = largeVector.map(_ + 1) @Benchmark def mapLargeList: List[Int] = largeList.map(_ + 1) + @Benchmark def mapLargeChain: Chain[Int] = largeChain.map(_ + 1) + @Benchmark def foldLeftSmallCatenable: Int = smallCatenable.foldLeft(0)(_ + _) @Benchmark def foldLeftSmallVector: Int = smallVector.foldLeft(0)(_ + _) @Benchmark def foldLeftSmallList: Int = smallList.foldLeft(0)(_ + _) + @Benchmark def foldLeftSmallChain: Int = smallChain.foldLeft(0)(_ + _) + @Benchmark def foldLeftLargeCatenable: Int = largeCatenable.foldLeft(0)(_ + _) @Benchmark def foldLeftLargeVector: Int = largeVector.foldLeft(0)(_ + _) @Benchmark def foldLeftLargeList: Int = largeList.foldLeft(0)(_ + _) + @Benchmark def foldLeftLargeChain: Int = largeChain.foldLeft(0)(_ + _) + @Benchmark def consSmallCatenable: Catenable[Int] = 0 +: smallCatenable @Benchmark def consSmallVector: Vector[Int] = 0 +: smallVector @Benchmark def consSmallList: List[Int] = 0 +: smallList + @Benchmark def consSmallChain: Chain[Int] = 0 +: smallChain + @Benchmark def consLargeCatenable: Catenable[Int] = 0 +: largeCatenable @Benchmark def consLargeVector: Vector[Int] = 0 +: largeVector @Benchmark def consLargeList: List[Int] = 0 +: largeList + @Benchmark def consLargeChain: Chain[Int] = 0 +: largeChain + @Benchmark def createTinyCatenable: Catenable[Int] = Catenable(1) @Benchmark def createTinyVector: Vector[Int] = Vector(1) @Benchmark def createTinyList: List[Int] = List(1) + @Benchmark def createTinyChain: Chain[Int] = Chain.single(1) + @Benchmark def createSmallCatenable: Catenable[Int] = Catenable(1, 2, 3, 4, 5) @Benchmark def createSmallVector: Vector[Int] = Vector(1, 2, 3, 4, 5) @Benchmark def createSmallList: List[Int] = List(1, 2, 3, 4, 5) + @Benchmark def createSmallChain: Chain[Int] = Chain(Seq(1, 2, 3, 4, 5)) } diff --git a/core/src/main/scala/cats/data/Catenable.scala b/core/src/main/scala/cats/data/Catenable.scala index d865123a07..5a1b7c499b 100644 --- a/core/src/main/scala/cats/data/Catenable.scala +++ b/core/src/main/scala/cats/data/Catenable.scala @@ -249,6 +249,9 @@ object Catenable extends CatenableInstances { /** Creates a catenable of 1 element. */ def singleton[A](a: A): Catenable[A] = Singleton(a) + /** Alias for singleton */ + def one[A](a: A): Catenable[A] = singleton(a) + /** Appends two catenables. */ def append[A](c: Catenable[A], c2: Catenable[A]): Catenable[A] = if (c.isEmpty) c2 @@ -279,6 +282,7 @@ object Catenable extends CatenableInstances { case _ => fromSeq(as) } + // scalastyle:off null class CatenableIterator[A](self: Catenable[A]) extends Iterator[A] { var c: Catenable[A] = if (self.isEmpty) null else self val rights = new collection.mutable.ArrayBuffer[Catenable[A]] @@ -303,6 +307,7 @@ object Catenable extends CatenableInstances { go } } + // scalastyle:on null } private[data] sealed abstract class CatenableInstances { diff --git a/laws/src/main/scala/cats/laws/discipline/Arbitrary.scala b/laws/src/main/scala/cats/laws/discipline/Arbitrary.scala index 130966a9fd..dc287f781b 100644 --- a/laws/src/main/scala/cats/laws/discipline/Arbitrary.scala +++ b/laws/src/main/scala/cats/laws/discipline/Arbitrary.scala @@ -279,9 +279,9 @@ object arbitrary extends ArbitraryInstances0 { implicit def catsLawsArbitraryForCatenable[A](implicit A: Arbitrary[A]): Arbitrary[Catenable[A]] = Arbitrary(Gen.sized { case 0 => Gen.const(Catenable.nil) - case 1 => A.arbitrary.map(Catenable.singleton) + case 1 => A.arbitrary.map(Catenable.one) case 2 => A.arbitrary.flatMap(a1 => A.arbitrary.flatMap(a2 => - Catenable.append(Catenable.singleton(a1), Catenable.singleton(a2)))) + Catenable.append(Catenable.one(a1), Catenable.one(a2)))) case n => Catenable.fromSeq(Range.apply(0, n)).foldLeft(Gen.const(Catenable.empty[A])) { (gen, _) => gen.flatMap(cat => A.arbitrary.map(a => cat :+ a)) } From cb458ab42b6103a327cc5090a1d67777adc6cb44 Mon Sep 17 00:00:00 2001 From: Luka Jacobowitz Date: Thu, 9 Aug 2018 12:43:28 +0200 Subject: [PATCH 08/19] Add Paul and Pavel to Authors and change COPYING to Cats Contributors --- AUTHORS.md | 2 ++ COPYING | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/AUTHORS.md b/AUTHORS.md index 6c21099b34..566870a664 100644 --- a/AUTHORS.md +++ b/AUTHORS.md @@ -149,8 +149,10 @@ possible: * P. Oscar Boykin * Paolo G. Giarrusso * Pascal Voitot + * Paul Chiusano * Paul Phillips * Paulo "JCranky" Siqueira + * Pavel Chlupacek * Pavkin Vladimir * Pepe GarcĂ­a * Pere Villega diff --git a/COPYING b/COPYING index 2bbc695e99..6e5add81f8 100644 --- a/COPYING +++ b/COPYING @@ -1,4 +1,4 @@ -Cats Copyright (c) 2015 Erik Osheim. +Cats Copyright (c) 2015 Cats Contributors. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in From 55cb945a497e899f21c41d48a7f82f0cac5d965b Mon Sep 17 00:00:00 2001 From: Luka Jacobowitz Date: Thu, 9 Aug 2018 13:38:59 +0200 Subject: [PATCH 09/19] More Tests --- .../scala/cats/tests/CatenableSuite.scala | 22 +++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/tests/src/test/scala/cats/tests/CatenableSuite.scala b/tests/src/test/scala/cats/tests/CatenableSuite.scala index 06a994e676..b89b67c377 100644 --- a/tests/src/test/scala/cats/tests/CatenableSuite.scala +++ b/tests/src/test/scala/cats/tests/CatenableSuite.scala @@ -33,21 +33,39 @@ class CatenableSuite extends CatsSuite { } } - test("filterNot and then forall should always be false") { + test("filterNot and then exists should always be false") { forAll { (ci: Catenable[Int], f: Int => Boolean) => ci.filterNot(f).exists(f) === false } } + test("filter and then forall should always be true") { + forAll { (ci: Catenable[Int], f: Int => Boolean) => + ci.filter(f).forall(f) === true + } + } + test("exists should be consistent with find + isDefined") { forAll { (ci: Catenable[Int], f: Int => Boolean) => ci.exists(f) === ci.find(f).isDefined } } + test("deleteFirst consistent with find") { + forAll { (ci: Catenable[Int], f: Int => Boolean) => + ci.find(f) === ci.deleteFirst(f).map(_._1) + } + } + + test("filterNot element and then contains should be false") { + forAll { (ci: Catenable[Int], i: Int) => + ci.filterNot(_ === i).contains(i) === false + } + } + test("Always nonempty after cons") { forAll { (ci: Catenable[Int], i: Int) => - ci.cons(i).nonEmpty === true + (i +: ci).nonEmpty === true } } From 79f22b2817de189943b5a1c3178ed54ff5e8ae70 Mon Sep 17 00:00:00 2001 From: Luka Jacobowitz Date: Thu, 9 Aug 2018 18:15:25 +0200 Subject: [PATCH 10/19] Add reverse, groupBy and zipWith --- core/src/main/scala/cats/data/Catenable.scala | 57 ++++++++++++++++++- .../scala/cats/tests/CatenableSuite.scala | 40 ++++++++++--- 2 files changed, 87 insertions(+), 10 deletions(-) diff --git a/core/src/main/scala/cats/data/Catenable.scala b/core/src/main/scala/cats/data/Catenable.scala index 5a1b7c499b..597b036e57 100644 --- a/core/src/main/scala/cats/data/Catenable.scala +++ b/core/src/main/scala/cats/data/Catenable.scala @@ -5,6 +5,7 @@ import cats.implicits._ import Catenable._ import scala.annotation.tailrec +import scala.collection.{SortedMap, mutable} /** * Trivial catenable sequence. Supports O(1) append, and (amortized) @@ -122,12 +123,64 @@ sealed abstract class Catenable[+A] { } /** Check whether all elements satisfy the predicate */ - final def forall(f: A => Boolean): Boolean = - exists(a => !f(a)) + final def forall(f: A => Boolean): Boolean = { + var result: Boolean = true + foreachUntil { a => + val b = f(a) + if (!b) result = false + !b + } + result + } + /** Check whether an element is in this structure */ final def contains[AA >: A](a: AA)(implicit A: Eq[AA]): Boolean = exists(A.eqv(a, _)) + /** Zips this `Catenable` with another `Catenable` and applies a function for each pair of elements. */ + final def zipWith[B, C](other: Catenable[B])(f: (A, B) => C): Catenable[C] = + if (this.isEmpty || other.isEmpty) Catenable.Empty + else { + val iterA = iterator + val iterB = other.iterator + + var result: Catenable[C] = Catenable.one(f(iterA.next(), iterB.next())) + + while (iterA.hasNext && iterB.hasNext) { + result = result :+ f(iterA.next(), iterB.next()) + } + result + } + + /** + * Groups elements inside this `Catenable` according to the `Order` + * of the keys produced by the given mapping function. + */ + final def groupBy[B](f: A => B)(implicit B: Order[B]): SortedMap[B, Catenable[A]] = { + implicit val ordering: Ordering[B] = B.toOrdering + var m = mutable.TreeMap.empty[B, Catenable[A]] + + foreach { elem => + val k = f(elem) + + m.get(k) match { + case None => m += ((k, singleton(elem))); () + case Some(cat) => m.update(k, cat :+ elem) + } + } + m + } + + /** Reverses this `Catenable` */ + def reverse: Catenable[A] = { + var result: Catenable[A] = Catenable.empty + foreach { a => + result = a +: result + () + } + result + } + /** * Yields to Some(a, Catenable[A]) with `a` removed where `f` holds for the first time, diff --git a/tests/src/test/scala/cats/tests/CatenableSuite.scala b/tests/src/test/scala/cats/tests/CatenableSuite.scala index b89b67c377..28fdfa4b9e 100644 --- a/tests/src/test/scala/cats/tests/CatenableSuite.scala +++ b/tests/src/test/scala/cats/tests/CatenableSuite.scala @@ -35,49 +35,73 @@ class CatenableSuite extends CatsSuite { test("filterNot and then exists should always be false") { forAll { (ci: Catenable[Int], f: Int => Boolean) => - ci.filterNot(f).exists(f) === false + ci.filterNot(f).exists(f) should === (false) } } test("filter and then forall should always be true") { forAll { (ci: Catenable[Int], f: Int => Boolean) => - ci.filter(f).forall(f) === true + ci.filter(f).forall(f) should === (true) } } test("exists should be consistent with find + isDefined") { forAll { (ci: Catenable[Int], f: Int => Boolean) => - ci.exists(f) === ci.find(f).isDefined + ci.exists(f) should === (ci.find(f).isDefined) } } test("deleteFirst consistent with find") { forAll { (ci: Catenable[Int], f: Int => Boolean) => - ci.find(f) === ci.deleteFirst(f).map(_._1) + ci.find(f) should === (ci.deleteFirst(f).map(_._1)) } } test("filterNot element and then contains should be false") { forAll { (ci: Catenable[Int], i: Int) => - ci.filterNot(_ === i).contains(i) === false + ci.filterNot(_ === i).contains(i) should === (false) } } test("Always nonempty after cons") { forAll { (ci: Catenable[Int], i: Int) => - (i +: ci).nonEmpty === true + (i +: ci).nonEmpty should === (true) } } test("fromSeq . toVector is id") { forAll { (ci: Catenable[Int]) => - Catenable.fromSeq(ci.toVector) === ci + Catenable.fromSeq(ci.toVector) should === (ci) } } test("fromSeq . toList . iterator is id") { forAll { (ci: Catenable[Int]) => - Catenable.fromSeq(ci.iterator.toList) === ci + Catenable.fromSeq(ci.iterator.toList) should === (ci) + } + } + + test("zipWith consistent with List#zip and then List#map") { + forAll { (a: Catenable[String], b: Catenable[Int], f: (String, Int) => Int) => + a.zipWith(b)(f).toList should === (a.toList.zip(b.toList).map { case (x, y) => f(x, y) }) + } + } + + test("groupBy consistent with List#groupBy") { + forAll { (cs: Catenable[String], f: String => Int) => + cs.groupBy(f).map { case (k, v) => (k, v.toList) }.toMap should === (cs.toList.groupBy(f).toMap) + } + } + + test("reverse . reverse is id") { + forAll { (ci: Catenable[Int]) => + ci.reverse.reverse should === (ci) + } + } + + test("reverse consistent with List#reverse") { + forAll { (ci: Catenable[Int]) => + ci.reverse.toList should === (ci.toList.reverse) } } } From 4108f6a6db8a0705c4d342ae399b7497b85480b3 Mon Sep 17 00:00:00 2001 From: Luka Jacobowitz Date: Fri, 10 Aug 2018 12:32:08 +0200 Subject: [PATCH 11/19] Add Collection Wrapping optimization --- .../scala/cats/bench/CatenableBench.scala | 20 +++- build.sbt | 3 +- core/src/main/scala/cats/data/Catenable.scala | 110 ++++++++++++------ 3 files changed, 94 insertions(+), 39 deletions(-) diff --git a/bench/src/main/scala/cats/bench/CatenableBench.scala b/bench/src/main/scala/cats/bench/CatenableBench.scala index a5a2793580..23aa61602b 100644 --- a/bench/src/main/scala/cats/bench/CatenableBench.scala +++ b/bench/src/main/scala/cats/bench/CatenableBench.scala @@ -1,6 +1,7 @@ package cats.bench import cats.data.Catenable +import fs2.{Catenable => OldCat} import chain.Chain import org.openjdk.jmh.annotations.{Benchmark, Scope, State} @@ -8,55 +9,70 @@ import org.openjdk.jmh.annotations.{Benchmark, Scope, State} class CatenableBench { private val smallCatenable = Catenable(1, 2, 3, 4, 5) + private val smallFs2Catenable = OldCat(1, 2, 3, 4, 5) private val smallVector = Vector(1, 2, 3, 4, 5) private val smallList = List(1, 2, 3, 4, 5) private val smallChain = Chain(smallList) - private val largeCatenable = Catenable.fromSeq(0 to 1000000) + private val largeCatenable = (0 to 1000) + .foldLeft(Catenable.empty[Int])((acc, _) => acc ++ Catenable.fromSeq(0 to 1000)) + private val largeFs2Catenable = OldCat.fromSeq(0 to 1000000) private val largeVector = (0 to 1000000).toVector private val largeList = (0 to 1000000).toList private val largeChain = (0 to 1000).foldLeft(Chain.empty[Int])((acc, _) => acc ++ Chain(0 to 1000)) @Benchmark def mapSmallCatenable: Catenable[Int] = smallCatenable.map(_ + 1) + @Benchmark def mapSmallFs2Catenable: OldCat[Int] = smallFs2Catenable.map(_ + 1) @Benchmark def mapSmallVector: Vector[Int] = smallVector.map(_ + 1) @Benchmark def mapSmallList: List[Int] = smallList.map(_ + 1) @Benchmark def mapSmallChain: Chain[Int] = smallChain.map(_ + 1) + @Benchmark def mapLargeCatenable: Catenable[Int] = largeCatenable.map(_ + 1) + @Benchmark def mapLargeFs2Catenable: OldCat[Int] = largeFs2Catenable.map(_ + 1) @Benchmark def mapLargeVector: Vector[Int] = largeVector.map(_ + 1) @Benchmark def mapLargeList: List[Int] = largeList.map(_ + 1) @Benchmark def mapLargeChain: Chain[Int] = largeChain.map(_ + 1) + @Benchmark def foldLeftSmallCatenable: Int = smallCatenable.foldLeft(0)(_ + _) + @Benchmark def foldLeftSmallFs2Catenable: Int = smallFs2Catenable.foldLeft(0)(_ + _) @Benchmark def foldLeftSmallVector: Int = smallVector.foldLeft(0)(_ + _) @Benchmark def foldLeftSmallList: Int = smallList.foldLeft(0)(_ + _) @Benchmark def foldLeftSmallChain: Int = smallChain.foldLeft(0)(_ + _) + @Benchmark def foldLeftLargeCatenable: Int = largeCatenable.foldLeft(0)(_ + _) + @Benchmark def foldLeftLargeFs2Catenable: Int = largeFs2Catenable.foldLeft(0)(_ + _) @Benchmark def foldLeftLargeVector: Int = largeVector.foldLeft(0)(_ + _) @Benchmark def foldLeftLargeList: Int = largeList.foldLeft(0)(_ + _) @Benchmark def foldLeftLargeChain: Int = largeChain.foldLeft(0)(_ + _) + + @Benchmark def consSmallCatenable: Catenable[Int] = 0 +: smallCatenable + @Benchmark def consSmallFs2Catenable: OldCat[Int] = 0 +: smallFs2Catenable @Benchmark def consSmallVector: Vector[Int] = 0 +: smallVector @Benchmark def consSmallList: List[Int] = 0 +: smallList @Benchmark def consSmallChain: Chain[Int] = 0 +: smallChain @Benchmark def consLargeCatenable: Catenable[Int] = 0 +: largeCatenable + @Benchmark def consLargeFs2Catenable: OldCat[Int] = 0 +: largeFs2Catenable @Benchmark def consLargeVector: Vector[Int] = 0 +: largeVector @Benchmark def consLargeList: List[Int] = 0 +: largeList @Benchmark def consLargeChain: Chain[Int] = 0 +: largeChain - @Benchmark def createTinyCatenable: Catenable[Int] = Catenable(1) + @Benchmark def createTinyFs2Catenable: OldCat[Int] = OldCat(1) @Benchmark def createTinyVector: Vector[Int] = Vector(1) @Benchmark def createTinyList: List[Int] = List(1) @Benchmark def createTinyChain: Chain[Int] = Chain.single(1) @Benchmark def createSmallCatenable: Catenable[Int] = Catenable(1, 2, 3, 4, 5) + @Benchmark def createSmallFs2Catenable: OldCat[Int] = OldCat(1, 2, 3, 4, 5) @Benchmark def createSmallVector: Vector[Int] = Vector(1, 2, 3, 4, 5) @Benchmark def createSmallList: List[Int] = List(1, 2, 3, 4, 5) @Benchmark def createSmallChain: Chain[Int] = Chain(Seq(1, 2, 3, 4, 5)) diff --git a/build.sbt b/build.sbt index 38211c288e..fee016a7fa 100644 --- a/build.sbt +++ b/build.sbt @@ -492,7 +492,8 @@ lazy val bench = project.dependsOn(macrosJVM, coreJVM, freeJVM, lawsJVM) .settings(coverageEnabled := false) .settings(libraryDependencies ++= Seq( "org.scalaz" %% "scalaz-core" % "7.2.23", - "org.spire-math" %% "chain" % "0.3.0" + "org.spire-math" %% "chain" % "0.3.0", + "co.fs2" %% "fs2-core" % "0.10.4" )) .enablePlugins(JmhPlugin) diff --git a/core/src/main/scala/cats/data/Catenable.scala b/core/src/main/scala/cats/data/Catenable.scala index 597b036e57..8d3fcaf4ef 100644 --- a/core/src/main/scala/cats/data/Catenable.scala +++ b/core/src/main/scala/cats/data/Catenable.scala @@ -5,7 +5,7 @@ import cats.implicits._ import Catenable._ import scala.annotation.tailrec -import scala.collection.{SortedMap, mutable} +import scala.collection.immutable.SortedMap /** * Trivial catenable sequence. Supports O(1) append, and (amortized) @@ -23,6 +23,16 @@ sealed abstract class Catenable[+A] { var result: Option[(A, Catenable[A])] = null while (result eq null) { c match { + case Singleton(a) => + val next = + if (rights.isEmpty) nil + else rights.reduceLeft((x, y) => Append(y, x)) + result = Some(a -> next) + case Append(l, r) => c = l; rights += r + case Wrap(seq) => + val tail = seq.tail + val next = fromSeq(tail) + result = Some(seq.head -> next) case Empty => if (rights.isEmpty) { result = None @@ -30,12 +40,6 @@ sealed abstract class Catenable[+A] { c = rights.last rights.trimEnd(1) } - case Singleton(a) => - val next = - if (rights.isEmpty) nil - else rights.reduceLeft((x, y) => Append(y, x)) - result = Some(a -> next) - case Append(l, r) => c = l; rights += r } } // scalastyle:on null @@ -70,7 +74,7 @@ sealed abstract class Catenable[+A] { /** Applies the supplied function to each element and returns a new catenable. */ final def map[B](f: A => B): Catenable[B] = - foldLeft(nil: Catenable[B])((acc, a) => acc :+ f(a)) + fromSeq(iterator.map(f).toVector) /** Applies the supplied function to each element and returns a new catenable from the concatenated results */ final def flatMap[B](f: A => Catenable[B]): Catenable[B] = @@ -158,14 +162,14 @@ sealed abstract class Catenable[+A] { */ final def groupBy[B](f: A => B)(implicit B: Order[B]): SortedMap[B, Catenable[A]] = { implicit val ordering: Ordering[B] = B.toOrdering - var m = mutable.TreeMap.empty[B, Catenable[A]] + var m = SortedMap.empty[B, Catenable[A]] foreach { elem => val k = f(elem) m.get(k) match { case None => m += ((k, singleton(elem))); () - case Some(cat) => m.update(k, cat :+ elem) + case Some(cat) => m = m.updated(k, cat :+ elem) } } m @@ -204,19 +208,13 @@ sealed abstract class Catenable[+A] { private final def foreach(f: A => Unit): Unit = foreachUntil { a => f(a); false } /** Applies the supplied function to each element, left to right, but stops when true is returned */ + // scalastyle:off null return cyclomatic.complexity private final def foreachUntil(f: A => Boolean): Unit = { var c: Catenable[A] = this val rights = new collection.mutable.ArrayBuffer[Catenable[A]] - // scalastyle:off null return + while (c ne null) { c match { - case Empty => - if (rights.isEmpty) { - c = null - } else { - c = rights.last - rights.trimEnd(1) - } case Singleton(a) => val b = f(a) if (b) return (); @@ -225,13 +223,33 @@ sealed abstract class Catenable[+A] { else rights.reduceLeft((x, y) => Append(y, x)) rights.clear() case Append(l, r) => c = l; rights += r + case Wrap(seq) => + val iterator = seq.iterator + while (iterator.hasNext) { + val b = f(iterator.next) + if (b) return () + } + c = + if (rights.isEmpty) Empty + else rights.reduceLeft((x, y) => Append(y, x)) + rights.clear() + case Empty => + if (rights.isEmpty) { + c = null + } else { + c = rights.last + rights.trimEnd(1) + } } } - // scalastyle:on null return } + // scalastyle:on null return cyclomatic.complexity - final def iterator: Iterator[A] = new CatenableIterator[A](this) + final def iterator: Iterator[A] = this match { + case Wrap(seq) => seq.iterator + case _ => new CatenableIterator[A](this) + } /** Returns the number of elements in this structure */ final def length: Int = { @@ -293,6 +311,10 @@ object Catenable extends CatenableInstances { def isEmpty: Boolean = false // b/c `append` constructor doesn't allow either branch to be empty } + private[data] final case class Wrap[A](seq: Seq[A]) extends Catenable[A] { + override def isEmpty: Boolean = + false // b/c `fromSeq` constructor doesn't allow either branch to be empty + } /** Empty catenable. */ val nil: Catenable[Nothing] = Empty @@ -314,7 +336,7 @@ object Catenable extends CatenableInstances { /** Creates a catenable from the specified sequence. */ def fromSeq[A](s: Seq[A]): Catenable[A] = if (s.isEmpty) nil - else s.view.reverse.map(singleton).reduceLeft((x, y) => Append(y, x)) + else Wrap(s) /** Creates a catenable from the specified elements. */ def apply[A](as: A*): Catenable[A] = @@ -339,24 +361,40 @@ object Catenable extends CatenableInstances { class CatenableIterator[A](self: Catenable[A]) extends Iterator[A] { var c: Catenable[A] = if (self.isEmpty) null else self val rights = new collection.mutable.ArrayBuffer[Catenable[A]] + var currentIterator: Iterator[A] = null - override def hasNext: Boolean = c ne null + override def hasNext: Boolean = (c ne null) || ((currentIterator ne null) && currentIterator.hasNext) override def next(): A = { - @tailrec def go: A = c match { - case Empty => - go // This can't happen - case Singleton(a) => - c = - if (rights.isEmpty) null - else rights.reduceLeft((x, y) => Append(y, x)) - rights.clear() - a - case Append(l, r) => - c = l - rights += r - go - } + @tailrec def go: A = + if ((currentIterator ne null) && currentIterator.hasNext) + currentIterator.next() + else { + currentIterator = null + + c match { + case Singleton(a) => + c = + if (rights.isEmpty) null + else rights.reduceLeft((x, y) => Append(y, x)) + rights.clear() + a + case Append(l, r) => + c = l + rights += r + go + case Wrap(seq) => + c = + if (rights.isEmpty) null + else rights.reduceLeft((x, y) => Append(y, x)) + rights.clear() + currentIterator = seq.iterator + currentIterator.next + case Empty => + go // This shouldn't happen + } + } + go } } From 534bf3d344dec094a9218f8fff1740cfc076c4ba Mon Sep 17 00:00:00 2001 From: Luka Jacobowitz Date: Sat, 11 Aug 2018 12:03:20 +0200 Subject: [PATCH 12/19] Add traverse and foldRight implementations that don't convert to List --- core/src/main/scala/cats/data/Catenable.scala | 23 +++++++++++++++---- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/core/src/main/scala/cats/data/Catenable.scala b/core/src/main/scala/cats/data/Catenable.scala index 8d3fcaf4ef..8dd2a8198e 100644 --- a/core/src/main/scala/cats/data/Catenable.scala +++ b/core/src/main/scala/cats/data/Catenable.scala @@ -5,6 +5,7 @@ import cats.implicits._ import Catenable._ import scala.annotation.tailrec +import scala.collection.mutable.ArrayStack import scala.collection.immutable.SortedMap /** @@ -87,6 +88,15 @@ sealed abstract class Catenable[+A] { result } + /** Folds over the elements from right to left using the supplied initial value and function. */ + final def foldRight[B](z: B)(f: (A, B) => B): B = { + val stack = new ArrayStack[A] + foreach { a => stack += a; () } + var result = z + while (!stack.isEmpty) { result = f(stack.pop, result) } + result + } + /** Collect `B` from this for which `f` is defined */ final def collect[B](pf: PartialFunction[A, B]): Catenable[B] = foldLeft(Catenable.nil: Catenable[B]) { (acc, a) => @@ -411,15 +421,18 @@ private[data] sealed abstract class CatenableInstances { new Traverse[Catenable] with Alternative[Catenable] with Monad[Catenable] { def foldLeft[A, B](fa: Catenable[A], b: B)(f: (B, A) => B): B = fa.foldLeft(b)(f) - def foldRight[A, B](fa: Catenable[A], b: Eval[B])(f: (A, Eval[B]) => Eval[B]): Eval[B] = - Foldable[List].foldRight(fa.toList, b)(f) + def foldRight[A, B](fa: Catenable[A], lb: Eval[B])(f: (A, Eval[B]) => Eval[B]): Eval[B] = + Eval.defer(fa.foldRight(lb) { (a, lb) => + Eval.defer(f(a, lb)) + }) override def map[A, B](fa: Catenable[A])(f: A => B): Catenable[B] = fa.map(f) override def toList[A](fa: Catenable[A]): List[A] = fa.toList override def isEmpty[A](fa: Catenable[A]): Boolean = fa.isEmpty - def traverse[F[_], A, B](fa: Catenable[A])(f: A => F[B])( - implicit G: Applicative[F]): F[Catenable[B]] = - Traverse[List].traverse(fa.toList)(f).map(Catenable.apply) + def traverse[G[_], A, B](fa: Catenable[A])(f: A => G[B])(implicit G: Applicative[G]): G[Catenable[B]] = + fa.foldLeft[G[Catenable[B]]](G.pure(nil)) { (gcatb, a) => + G.map2(gcatb, f(a))(_ :+ _) + } def empty[A]: Catenable[A] = Catenable.nil def combineK[A](c: Catenable[A], c2: Catenable[A]): Catenable[A] = Catenable.append(c, c2) def pure[A](a: A): Catenable[A] = Catenable.singleton(a) From 1dd44e4b401451d634009ca615dcc684854c625d Mon Sep 17 00:00:00 2001 From: Luka Jacobowitz Date: Sun, 12 Aug 2018 12:00:17 +0200 Subject: [PATCH 13/19] Rename to Chain; add reverseIterator --- ...{CatenableBench.scala => ChainBench.scala} | 52 ++-- .../cats/bench/CollectionMonoidBench.scala | 14 +- .../data/{Catenable.scala => Chain.scala} | 275 ++++++++++-------- .../cats/laws/discipline/Arbitrary.scala | 10 +- ...{CatenableSuite.scala => ChainSuite.scala} | 56 ++-- 5 files changed, 220 insertions(+), 187 deletions(-) rename bench/src/main/scala/cats/bench/{CatenableBench.scala => ChainBench.scala} (65%) rename core/src/main/scala/cats/data/{Catenable.scala => Chain.scala} (55%) rename tests/src/test/scala/cats/tests/{CatenableSuite.scala => ChainSuite.scala} (50%) diff --git a/bench/src/main/scala/cats/bench/CatenableBench.scala b/bench/src/main/scala/cats/bench/ChainBench.scala similarity index 65% rename from bench/src/main/scala/cats/bench/CatenableBench.scala rename to bench/src/main/scala/cats/bench/ChainBench.scala index 23aa61602b..a8e64b3409 100644 --- a/bench/src/main/scala/cats/bench/CatenableBench.scala +++ b/bench/src/main/scala/cats/bench/ChainBench.scala @@ -1,79 +1,79 @@ package cats.bench -import cats.data.Catenable -import fs2.{Catenable => OldCat} -import chain.Chain +import cats.data.Chain +import fs2.Catenable +import chain.{Chain => OldChain} import org.openjdk.jmh.annotations.{Benchmark, Scope, State} @State(Scope.Thread) -class CatenableBench { +class ChainBench { + private val smallChain = Chain(1, 2, 3, 4, 5) private val smallCatenable = Catenable(1, 2, 3, 4, 5) - private val smallFs2Catenable = OldCat(1, 2, 3, 4, 5) private val smallVector = Vector(1, 2, 3, 4, 5) private val smallList = List(1, 2, 3, 4, 5) - private val smallChain = Chain(smallList) + private val smallOldChain = OldChain(smallList) - private val largeCatenable = (0 to 1000) - .foldLeft(Catenable.empty[Int])((acc, _) => acc ++ Catenable.fromSeq(0 to 1000)) - private val largeFs2Catenable = OldCat.fromSeq(0 to 1000000) + private val largeChain = (0 to 1000) + .foldLeft(Chain.empty[Int])((acc, _) => acc ++ Chain.fromSeq(0 to 1000)) + private val largeCatenable = Catenable.fromSeq(0 to 1000000) private val largeVector = (0 to 1000000).toVector private val largeList = (0 to 1000000).toList - private val largeChain = (0 to 1000).foldLeft(Chain.empty[Int])((acc, _) => acc ++ Chain(0 to 1000)) + private val largeOldChain = (0 to 1000).foldLeft(OldChain.empty[Int])((acc, _) => acc ++ OldChain(0 to 1000)) + @Benchmark def mapSmallChain: Chain[Int] = smallChain.map(_ + 1) @Benchmark def mapSmallCatenable: Catenable[Int] = smallCatenable.map(_ + 1) - @Benchmark def mapSmallFs2Catenable: OldCat[Int] = smallFs2Catenable.map(_ + 1) @Benchmark def mapSmallVector: Vector[Int] = smallVector.map(_ + 1) @Benchmark def mapSmallList: List[Int] = smallList.map(_ + 1) - @Benchmark def mapSmallChain: Chain[Int] = smallChain.map(_ + 1) + @Benchmark def mapSmallOldChain: OldChain[Int] = smallOldChain.map(_ + 1) + @Benchmark def mapLargeChain: Chain[Int] = largeChain.map(_ + 1) @Benchmark def mapLargeCatenable: Catenable[Int] = largeCatenable.map(_ + 1) - @Benchmark def mapLargeFs2Catenable: OldCat[Int] = largeFs2Catenable.map(_ + 1) @Benchmark def mapLargeVector: Vector[Int] = largeVector.map(_ + 1) @Benchmark def mapLargeList: List[Int] = largeList.map(_ + 1) - @Benchmark def mapLargeChain: Chain[Int] = largeChain.map(_ + 1) + @Benchmark def mapLargeOldChain: OldChain[Int] = largeOldChain.map(_ + 1) + @Benchmark def foldLeftSmallChain: Int = smallChain.foldLeft(0)(_ + _) @Benchmark def foldLeftSmallCatenable: Int = smallCatenable.foldLeft(0)(_ + _) - @Benchmark def foldLeftSmallFs2Catenable: Int = smallFs2Catenable.foldLeft(0)(_ + _) @Benchmark def foldLeftSmallVector: Int = smallVector.foldLeft(0)(_ + _) @Benchmark def foldLeftSmallList: Int = smallList.foldLeft(0)(_ + _) - @Benchmark def foldLeftSmallChain: Int = smallChain.foldLeft(0)(_ + _) + @Benchmark def foldLeftSmallOldChain: Int = smallOldChain.foldLeft(0)(_ + _) + @Benchmark def foldLeftLargeChain: Int = largeChain.foldLeft(0)(_ + _) @Benchmark def foldLeftLargeCatenable: Int = largeCatenable.foldLeft(0)(_ + _) - @Benchmark def foldLeftLargeFs2Catenable: Int = largeFs2Catenable.foldLeft(0)(_ + _) @Benchmark def foldLeftLargeVector: Int = largeVector.foldLeft(0)(_ + _) @Benchmark def foldLeftLargeList: Int = largeList.foldLeft(0)(_ + _) - @Benchmark def foldLeftLargeChain: Int = largeChain.foldLeft(0)(_ + _) + @Benchmark def foldLeftLargeOldChain: Int = largeOldChain.foldLeft(0)(_ + _) + @Benchmark def consSmallChain: Chain[Int] = 0 +: smallChain @Benchmark def consSmallCatenable: Catenable[Int] = 0 +: smallCatenable - @Benchmark def consSmallFs2Catenable: OldCat[Int] = 0 +: smallFs2Catenable @Benchmark def consSmallVector: Vector[Int] = 0 +: smallVector @Benchmark def consSmallList: List[Int] = 0 +: smallList - @Benchmark def consSmallChain: Chain[Int] = 0 +: smallChain + @Benchmark def consSmallOldChain: OldChain[Int] = 0 +: smallOldChain + @Benchmark def consLargeChain: Chain[Int] = 0 +: largeChain @Benchmark def consLargeCatenable: Catenable[Int] = 0 +: largeCatenable - @Benchmark def consLargeFs2Catenable: OldCat[Int] = 0 +: largeFs2Catenable @Benchmark def consLargeVector: Vector[Int] = 0 +: largeVector @Benchmark def consLargeList: List[Int] = 0 +: largeList - @Benchmark def consLargeChain: Chain[Int] = 0 +: largeChain + @Benchmark def consLargeOldChain: OldChain[Int] = 0 +: largeOldChain + @Benchmark def createTinyChain: Chain[Int] = Chain(1) @Benchmark def createTinyCatenable: Catenable[Int] = Catenable(1) - @Benchmark def createTinyFs2Catenable: OldCat[Int] = OldCat(1) @Benchmark def createTinyVector: Vector[Int] = Vector(1) @Benchmark def createTinyList: List[Int] = List(1) - @Benchmark def createTinyChain: Chain[Int] = Chain.single(1) + @Benchmark def createTinyOldChain: OldChain[Int] = OldChain.single(1) + @Benchmark def createSmallChain: Chain[Int] = Chain(1, 2, 3, 4, 5) @Benchmark def createSmallCatenable: Catenable[Int] = Catenable(1, 2, 3, 4, 5) - @Benchmark def createSmallFs2Catenable: OldCat[Int] = OldCat(1, 2, 3, 4, 5) @Benchmark def createSmallVector: Vector[Int] = Vector(1, 2, 3, 4, 5) @Benchmark def createSmallList: List[Int] = List(1, 2, 3, 4, 5) - @Benchmark def createSmallChain: Chain[Int] = Chain(Seq(1, 2, 3, 4, 5)) + @Benchmark def createSmallOldChain: OldChain[Int] = OldChain(Seq(1, 2, 3, 4, 5)) } diff --git a/bench/src/main/scala/cats/bench/CollectionMonoidBench.scala b/bench/src/main/scala/cats/bench/CollectionMonoidBench.scala index 4dfa6714d2..81d4ec1970 100644 --- a/bench/src/main/scala/cats/bench/CollectionMonoidBench.scala +++ b/bench/src/main/scala/cats/bench/CollectionMonoidBench.scala @@ -1,9 +1,9 @@ package cats.bench import cats.Monoid -import cats.data.Catenable +import cats.data.Chain import cats.implicits._ -import chain.Chain +import chain.{Chain => OldChain} import org.openjdk.jmh.annotations.{Benchmark, Scope, State} @State(Scope.Thread) @@ -11,14 +11,14 @@ class CollectionMonoidBench { private val largeList = (0 to 1000000).toList - implicit def monoidChain[A]: Monoid[Chain[A]] = new Monoid[Chain[A]] { - def empty: Chain[A] = Chain.empty[A] + implicit def monoidOldChain[A]: Monoid[OldChain[A]] = new Monoid[OldChain[A]] { + def empty: OldChain[A] = OldChain.empty[A] - def combine(x: Chain[A], y: Chain[A]): Chain[A] = x ++ y + def combine(x: OldChain[A], y: OldChain[A]): OldChain[A] = x ++ y } - @Benchmark def accumulateCatenable: Catenable[Int] = largeList.foldMap(Catenable.singleton) + @Benchmark def accumulateChain: Chain[Int] = largeList.foldMap(Chain.one) @Benchmark def accumulateVector: Vector[Int] = largeList.foldMap(Vector(_)) @Benchmark def accumulateList: List[Int] = largeList.foldMap(List(_)) - @Benchmark def accumulateChain: Chain[Int] = largeList.foldMap(Chain.single) + @Benchmark def accumulateOldChain: OldChain[Int] = largeList.foldMap(OldChain.single) } diff --git a/core/src/main/scala/cats/data/Catenable.scala b/core/src/main/scala/cats/data/Chain.scala similarity index 55% rename from core/src/main/scala/cats/data/Catenable.scala rename to core/src/main/scala/cats/data/Chain.scala index 8dd2a8198e..934e097f5c 100644 --- a/core/src/main/scala/cats/data/Catenable.scala +++ b/core/src/main/scala/cats/data/Chain.scala @@ -2,26 +2,24 @@ package cats package data import cats.implicits._ -import Catenable._ +import Chain._ import scala.annotation.tailrec -import scala.collection.mutable.ArrayStack import scala.collection.immutable.SortedMap /** * Trivial catenable sequence. Supports O(1) append, and (amortized) * O(1) `uncons`, such that walking the sequence via N successive `uncons` - * steps takes O(N). Like a difference list, conversion to a `Seq[A]` - * takes linear time, regardless of how the sequence is built up. + * steps takes O(N). */ -sealed abstract class Catenable[+A] { +sealed abstract class Chain[+A] { - /** Returns the head and tail of this catenable if non empty, none otherwise. Amortized O(1). */ - final def uncons: Option[(A, Catenable[A])] = { - var c: Catenable[A] = this - val rights = new collection.mutable.ArrayBuffer[Catenable[A]] + /** Returns the head and tail of this Chain if non empty, none otherwise. Amortized O(1). */ + final def uncons: Option[(A, Chain[A])] = { + var c: Chain[A] = this + val rights = new collection.mutable.ArrayBuffer[Chain[A]] // scalastyle:off null - var result: Option[(A, Catenable[A])] = null + var result: Option[(A, Chain[A])] = null while (result eq null) { c match { case Singleton(a) => @@ -33,7 +31,7 @@ sealed abstract class Catenable[+A] { case Wrap(seq) => val tail = seq.tail val next = fromSeq(tail) - result = Some(seq.head -> next) + result = Some((seq.head, next)) case Empty => if (rights.isEmpty) { result = None @@ -54,32 +52,32 @@ sealed abstract class Catenable[+A] { def nonEmpty: Boolean = !isEmpty /** Concatenates this with `c` in O(1) runtime. */ - final def ++[A2 >: A](c: Catenable[A2]): Catenable[A2] = + final def ++[A2 >: A](c: Chain[A2]): Chain[A2] = append(this, c) - /** Returns a new catenable consisting of `a` followed by this. O(1) runtime. */ - final def cons[A2 >: A](a: A2): Catenable[A2] = - append(singleton(a), this) + /** Returns a new Chain consisting of `a` followed by this. O(1) runtime. */ + final def cons[A2 >: A](a: A2): Chain[A2] = + append(one(a), this) /** Alias for [[cons]]. */ - final def +:[A2 >: A](a: A2): Catenable[A2] = + final def +:[A2 >: A](a: A2): Chain[A2] = cons(a) - /** Returns a new catenable consisting of this followed by `a`. O(1) runtime. */ - final def snoc[A2 >: A](a: A2): Catenable[A2] = - append(this, singleton(a)) + /** Returns a new Chain consisting of this followed by `a`. O(1) runtime. */ + final def snoc[A2 >: A](a: A2): Chain[A2] = + append(this, one(a)) /** Alias for [[snoc]]. */ - final def :+[A2 >: A](a: A2): Catenable[A2] = + final def :+[A2 >: A](a: A2): Chain[A2] = snoc(a) - /** Applies the supplied function to each element and returns a new catenable. */ - final def map[B](f: A => B): Catenable[B] = + /** Applies the supplied function to each element and returns a new Chain. */ + final def map[B](f: A => B): Chain[B] = fromSeq(iterator.map(f).toVector) - /** Applies the supplied function to each element and returns a new catenable from the concatenated results */ - final def flatMap[B](f: A => Catenable[B]): Catenable[B] = - foldLeft(nil: Catenable[B])((acc, a) => acc ++ f(a)) + /** Applies the supplied function to each element and returns a new Chain from the concatenated results */ + final def flatMap[B](f: A => Chain[B]): Chain[B] = + foldLeft(nil: Chain[B])((acc, a) => acc ++ f(a)) /** Folds over the elements from left to right using the supplied initial value and function. */ final def foldLeft[B](z: B)(f: (B, A) => B): B = { @@ -90,16 +88,15 @@ sealed abstract class Catenable[+A] { /** Folds over the elements from right to left using the supplied initial value and function. */ final def foldRight[B](z: B)(f: (A, B) => B): B = { - val stack = new ArrayStack[A] - foreach { a => stack += a; () } var result = z - while (!stack.isEmpty) { result = f(stack.pop, result) } + val iter = reverseIterator + while (iter.hasNext) { result = f(iter.next, result) } result } /** Collect `B` from this for which `f` is defined */ - final def collect[B](pf: PartialFunction[A, B]): Catenable[B] = - foldLeft(Catenable.nil: Catenable[B]) { (acc, a) => + final def collect[B](pf: PartialFunction[A, B]): Chain[B] = + foldLeft(Chain.nil: Chain[B]) { (acc, a) => // trick from TraversableOnce, used to avoid calling both isDefined and apply (or calling lift) val x = pf.applyOrElse(a, sentinel) if (x.asInstanceOf[AnyRef] ne sentinel) acc :+ x.asInstanceOf[B] @@ -107,11 +104,11 @@ sealed abstract class Catenable[+A] { } /** Remove elements not matching the predicate */ - final def filter(f: A => Boolean): Catenable[A] = + final def filter(f: A => Boolean): Chain[A] = collect { case a if f(a) => a } /** Remove elements matching the predicate */ - final def filterNot(f: A => Boolean): Catenable[A] = + final def filterNot(f: A => Boolean): Chain[A] = filter(a => !f(a)) /** Find the first element matching the predicate, if one exists */ @@ -151,14 +148,14 @@ sealed abstract class Catenable[+A] { final def contains[AA >: A](a: AA)(implicit A: Eq[AA]): Boolean = exists(A.eqv(a, _)) - /** Zips this `Catenable` with another `Catenable` and applies a function for each pair of elements. */ - final def zipWith[B, C](other: Catenable[B])(f: (A, B) => C): Catenable[C] = - if (this.isEmpty || other.isEmpty) Catenable.Empty + /** Zips this `Chain` with another `Chain` and applies a function for each pair of elements. */ + final def zipWith[B, C](other: Chain[B])(f: (A, B) => C): Chain[C] = + if (this.isEmpty || other.isEmpty) Chain.Empty else { val iterA = iterator val iterB = other.iterator - var result: Catenable[C] = Catenable.one(f(iterA.next(), iterB.next())) + var result: Chain[C] = Chain.one(f(iterA.next(), iterB.next())) while (iterA.hasNext && iterB.hasNext) { result = result :+ f(iterA.next(), iterB.next()) @@ -167,43 +164,38 @@ sealed abstract class Catenable[+A] { } /** - * Groups elements inside this `Catenable` according to the `Order` + * Groups elements inside this `Chain` according to the `Order` * of the keys produced by the given mapping function. */ - final def groupBy[B](f: A => B)(implicit B: Order[B]): SortedMap[B, Catenable[A]] = { + final def groupBy[B](f: A => B)(implicit B: Order[B]): SortedMap[B, Chain[A]] = { implicit val ordering: Ordering[B] = B.toOrdering - var m = SortedMap.empty[B, Catenable[A]] + var m = SortedMap.empty[B, Chain[A]] foreach { elem => val k = f(elem) m.get(k) match { - case None => m += ((k, singleton(elem))); () + case None => m += ((k, one(elem))); () case Some(cat) => m = m.updated(k, cat :+ elem) } } m } - /** Reverses this `Catenable` */ - def reverse: Catenable[A] = { - var result: Catenable[A] = Catenable.empty - foreach { a => - result = a +: result - () - } - result + /** Reverses this `Chain` */ + def reverse: Chain[A] = { + Wrap(reverseIterator.toList) } /** - * Yields to Some(a, Catenable[A]) with `a` removed where `f` holds for the first time, + * Yields to Some(a, Chain[A]) with `a` removed where `f` holds for the first time, * otherwise yields None, if `a` was not found * Traverses only until `a` is found. */ - final def deleteFirst(f: A => Boolean): Option[(A, Catenable[A])] = { + final def deleteFirst(f: A => Boolean): Option[(A, Chain[A])] = { @tailrec - def go(rem: Catenable[A], acc: Catenable[A]): Option[(A, Catenable[A])] = + def go(rem: Chain[A], acc: Chain[A]): Option[(A, Chain[A])] = rem.uncons match { case Some((a, tail)) => if (!f(a)) go(tail, acc :+ a) @@ -211,7 +203,7 @@ sealed abstract class Catenable[+A] { case None => None } - go(this, Catenable.nil) + go(this, Chain.nil) } /** Applies the supplied function to each element, left to right. */ @@ -220,8 +212,8 @@ sealed abstract class Catenable[+A] { /** Applies the supplied function to each element, left to right, but stops when true is returned */ // scalastyle:off null return cyclomatic.complexity private final def foreachUntil(f: A => Boolean): Unit = { - var c: Catenable[A] = this - val rights = new collection.mutable.ArrayBuffer[Catenable[A]] + var c: Chain[A] = this + val rights = new collection.mutable.ArrayBuffer[Chain[A]] while (c ne null) { c match { @@ -258,7 +250,12 @@ sealed abstract class Catenable[+A] { final def iterator: Iterator[A] = this match { case Wrap(seq) => seq.iterator - case _ => new CatenableIterator[A](this) + case _ => new ChainIterator[A](this) + } + + final def reverseIterator: Iterator[A] = this match { + case Wrap(seq) => seq.reverseIterator + case _ => new ChainReverseIterator[A](this) } /** Returns the number of elements in this structure */ @@ -273,25 +270,15 @@ sealed abstract class Catenable[+A] { /** Converts to a list. */ - final def toList: List[A] = { - val builder = List.newBuilder[A] - foreach { a => - builder += a; () - } - builder.result - } + final def toList: List[A] = + iterator.toList /** Converts to a vector. */ - final def toVector: Vector[A] = { - val builder = new scala.collection.immutable.VectorBuilder[A]() - foreach { a => - builder += a; () - } - builder.result - } + final def toVector: Vector[A] = + iterator.toVector def show[AA >: A](implicit AA: Show[AA]): String = { - val builder = new StringBuilder("Catenable(") + val builder = new StringBuilder("Chain(") var first = true foreach { a => @@ -306,60 +293,58 @@ sealed abstract class Catenable[+A] { override def toString: String = show(Show.show[A](_.toString)) } -object Catenable extends CatenableInstances { +object Chain extends ChainInstances { private val sentinel: Function1[Any, Any] = new scala.runtime.AbstractFunction1[Any, Any]{ def apply(a: Any) = this } - private[data] final case object Empty extends Catenable[Nothing] { + private[data] final case object Empty extends Chain[Nothing] { def isEmpty: Boolean = true } - private[data] final case class Singleton[A](a: A) extends Catenable[A] { + private[data] final case class Singleton[A](a: A) extends Chain[A] { def isEmpty: Boolean = false } - private[data] final case class Append[A](left: Catenable[A], right: Catenable[A]) - extends Catenable[A] { + private[data] final case class Append[A](left: Chain[A], right: Chain[A]) + extends Chain[A] { def isEmpty: Boolean = false // b/c `append` constructor doesn't allow either branch to be empty } - private[data] final case class Wrap[A](seq: Seq[A]) extends Catenable[A] { + private[data] final case class Wrap[A](seq: Seq[A]) extends Chain[A] { override def isEmpty: Boolean = false // b/c `fromSeq` constructor doesn't allow either branch to be empty } - /** Empty catenable. */ - val nil: Catenable[Nothing] = Empty - - def empty[A]: Catenable[A] = nil + /** Empty Chain. */ + val nil: Chain[Nothing] = Empty - /** Creates a catenable of 1 element. */ - def singleton[A](a: A): Catenable[A] = Singleton(a) + def empty[A]: Chain[A] = nil - /** Alias for singleton */ - def one[A](a: A): Catenable[A] = singleton(a) + /** Creates a Chain of 1 element. */ + def one[A](a: A): Chain[A] = Singleton(a) - /** Appends two catenables. */ - def append[A](c: Catenable[A], c2: Catenable[A]): Catenable[A] = + /** Appends two Chains. */ + def append[A](c: Chain[A], c2: Chain[A]): Chain[A] = if (c.isEmpty) c2 else if (c2.isEmpty) c else Append(c, c2) - /** Creates a catenable from the specified sequence. */ - def fromSeq[A](s: Seq[A]): Catenable[A] = + /** Creates a Chain from the specified sequence. */ + def fromSeq[A](s: Seq[A]): Chain[A] = if (s.isEmpty) nil + else if (s.lengthCompare(1) == 0) one(s.head) else Wrap(s) - /** Creates a catenable from the specified elements. */ - def apply[A](as: A*): Catenable[A] = + /** Creates a Chain from the specified elements. */ + def apply[A](as: A*): Chain[A] = as match { case w: collection.mutable.WrappedArray[A] => if (w.isEmpty) nil - else if (w.size == 1) singleton(w.head) + else if (w.size == 1) one(w.head) else { val arr: Array[A] = w.array - var c: Catenable[A] = singleton(arr.last) + var c: Chain[A] = one(arr.last) var idx = arr.size - 2 while (idx >= 0) { - c = Append(singleton(arr(idx)), c) + c = Append(one(arr(idx)), c) idx -= 1 } c @@ -368,9 +353,9 @@ object Catenable extends CatenableInstances { } // scalastyle:off null - class CatenableIterator[A](self: Catenable[A]) extends Iterator[A] { - var c: Catenable[A] = if (self.isEmpty) null else self - val rights = new collection.mutable.ArrayBuffer[Catenable[A]] + class ChainIterator[A](self: Chain[A]) extends Iterator[A] { + var c: Chain[A] = if (self.isEmpty) null else self + val rights = new collection.mutable.ArrayBuffer[Chain[A]] var currentIterator: Iterator[A] = null override def hasNext: Boolean = (c ne null) || ((currentIterator ne null) && currentIterator.hasNext) @@ -409,38 +394,86 @@ object Catenable extends CatenableInstances { } } // scalastyle:on null + + + // scalastyle:off null + class ChainReverseIterator[A](self: Chain[A]) extends Iterator[A] { + var c: Chain[A] = if (self.isEmpty) null else self + val lefts = new collection.mutable.ArrayBuffer[Chain[A]] + var currentIterator: Iterator[A] = null + + override def hasNext: Boolean = (c ne null) || ((currentIterator ne null) && currentIterator.hasNext) + + override def next(): A = { + @tailrec def go: A = + if ((currentIterator ne null) && currentIterator.hasNext) + currentIterator.next() + else { + currentIterator = null + + c match { + case Singleton(a) => + c = + if (lefts.isEmpty) null + else lefts.reduceLeft((x, y) => Append(x, y)) + lefts.clear() + a + case Append(l, r) => + c = r + lefts += l + go + case Wrap(seq) => + c = + if (lefts.isEmpty) null + else lefts.reduceLeft((x, y) => Append(x, y)) + lefts.clear() + currentIterator = seq.reverseIterator + currentIterator.next + case Empty => + go // This shouldn't happen + } + } + + go + } + } + // scalastyle:on null } -private[data] sealed abstract class CatenableInstances { - implicit def catsDataMonoidForCatenable[A]: Monoid[Catenable[A]] = new Monoid[Catenable[A]] { - def empty: Catenable[A] = Catenable.nil - def combine(c: Catenable[A], c2: Catenable[A]): Catenable[A] = Catenable.append(c, c2) +private[data] sealed abstract class ChainInstances { + implicit def catsDataMonoidForChain[A]: Monoid[Chain[A]] = new Monoid[Chain[A]] { + def empty: Chain[A] = Chain.nil + def combine(c: Chain[A], c2: Chain[A]): Chain[A] = Chain.append(c, c2) } - implicit val catsDataInstancesForCatenable: Traverse[Catenable] with Alternative[Catenable] with Monad[Catenable] = - new Traverse[Catenable] with Alternative[Catenable] with Monad[Catenable] { - def foldLeft[A, B](fa: Catenable[A], b: B)(f: (B, A) => B): B = + implicit val catsDataInstancesForChain: Traverse[Chain] with Alternative[Chain] with Monad[Chain] = + new Traverse[Chain] with Alternative[Chain] with Monad[Chain] { + def foldLeft[A, B](fa: Chain[A], b: B)(f: (B, A) => B): B = fa.foldLeft(b)(f) - def foldRight[A, B](fa: Catenable[A], lb: Eval[B])(f: (A, Eval[B]) => Eval[B]): Eval[B] = + def foldRight[A, B](fa: Chain[A], lb: Eval[B])(f: (A, Eval[B]) => Eval[B]): Eval[B] = Eval.defer(fa.foldRight(lb) { (a, lb) => Eval.defer(f(a, lb)) }) - override def map[A, B](fa: Catenable[A])(f: A => B): Catenable[B] = fa.map(f) - override def toList[A](fa: Catenable[A]): List[A] = fa.toList - override def isEmpty[A](fa: Catenable[A]): Boolean = fa.isEmpty - def traverse[G[_], A, B](fa: Catenable[A])(f: A => G[B])(implicit G: Applicative[G]): G[Catenable[B]] = - fa.foldLeft[G[Catenable[B]]](G.pure(nil)) { (gcatb, a) => - G.map2(gcatb, f(a))(_ :+ _) + override def map[A, B](fa: Chain[A])(f: A => B): Chain[B] = fa.map(f) + override def toList[A](fa: Chain[A]): List[A] = fa.toList + override def isEmpty[A](fa: Chain[A]): Boolean = fa.isEmpty + override def exists[A](fa: Chain[A])(p: A => Boolean): Boolean = fa.exists(p) + override def forall[A](fa: Chain[A])(p: A => Boolean): Boolean = fa.forall(p) + override def find[A](fa: Chain[A])(f: A => Boolean): Option[A] = fa.find(f) + + def traverse[G[_], A, B](fa: Chain[A])(f: A => G[B])(implicit G: Applicative[G]): G[Chain[B]] = + fa.foldRight[G[Chain[B]]](G.pure(nil)) { (a, gcatb) => + G.map2(f(a), gcatb)(_ +: _) } - def empty[A]: Catenable[A] = Catenable.nil - def combineK[A](c: Catenable[A], c2: Catenable[A]): Catenable[A] = Catenable.append(c, c2) - def pure[A](a: A): Catenable[A] = Catenable.singleton(a) - def flatMap[A, B](fa: Catenable[A])(f: A => Catenable[B]): Catenable[B] = + def empty[A]: Chain[A] = Chain.nil + def combineK[A](c: Chain[A], c2: Chain[A]): Chain[A] = Chain.append(c, c2) + def pure[A](a: A): Chain[A] = Chain.one(a) + def flatMap[A, B](fa: Chain[A])(f: A => Chain[B]): Chain[B] = fa.flatMap(f) - def tailRecM[A, B](a: A)(f: A => Catenable[Either[A, B]]): Catenable[B] = { - var acc: Catenable[B] = Catenable.nil - @tailrec def go(rest: List[Catenable[Either[A, B]]]): Unit = + def tailRecM[A, B](a: A)(f: A => Chain[Either[A, B]]): Chain[B] = { + var acc: Chain[B] = Chain.nil + @tailrec def go(rest: List[Chain[Either[A, B]]]): Unit = rest match { case hd :: tl => hd.uncons match { @@ -462,11 +495,11 @@ private[data] sealed abstract class CatenableInstances { } } - implicit def catsDataShowForCatenable[A](implicit A: Show[A]): Show[Catenable[A]] = - Show.show[Catenable[A]](_.show) + implicit def catsDataShowForChain[A](implicit A: Show[A]): Show[Chain[A]] = + Show.show[Chain[A]](_.show) - implicit def catsDataEqForCatenable[A](implicit A: Eq[A]): Eq[Catenable[A]] = new Eq[Catenable[A]] { - def eqv(x: Catenable[A], y: Catenable[A]): Boolean = + implicit def catsDataEqForChain[A](implicit A: Eq[A]): Eq[Chain[A]] = new Eq[Chain[A]] { + def eqv(x: Chain[A], y: Chain[A]): Boolean = (x eq y) || x.toList === y.toList } diff --git a/laws/src/main/scala/cats/laws/discipline/Arbitrary.scala b/laws/src/main/scala/cats/laws/discipline/Arbitrary.scala index dc287f781b..5f4452b755 100644 --- a/laws/src/main/scala/cats/laws/discipline/Arbitrary.scala +++ b/laws/src/main/scala/cats/laws/discipline/Arbitrary.scala @@ -276,13 +276,13 @@ object arbitrary extends ArbitraryInstances0 { implicit def catsLawsCogenForAndThen[A, B](implicit F: Cogen[A => B]): Cogen[AndThen[A, B]] = Cogen((seed, x) => F.perturb(seed, x)) - implicit def catsLawsArbitraryForCatenable[A](implicit A: Arbitrary[A]): Arbitrary[Catenable[A]] = + implicit def catsLawsArbitraryForChain[A](implicit A: Arbitrary[A]): Arbitrary[Chain[A]] = Arbitrary(Gen.sized { - case 0 => Gen.const(Catenable.nil) - case 1 => A.arbitrary.map(Catenable.one) + case 0 => Gen.const(Chain.nil) + case 1 => A.arbitrary.map(Chain.one) case 2 => A.arbitrary.flatMap(a1 => A.arbitrary.flatMap(a2 => - Catenable.append(Catenable.one(a1), Catenable.one(a2)))) - case n => Catenable.fromSeq(Range.apply(0, n)).foldLeft(Gen.const(Catenable.empty[A])) { (gen, _) => + Chain.append(Chain.one(a1), Chain.one(a2)))) + case n => Chain.fromSeq(Range.apply(0, n)).foldLeft(Gen.const(Chain.empty[A])) { (gen, _) => gen.flatMap(cat => A.arbitrary.map(a => cat :+ a)) } }) diff --git a/tests/src/test/scala/cats/tests/CatenableSuite.scala b/tests/src/test/scala/cats/tests/ChainSuite.scala similarity index 50% rename from tests/src/test/scala/cats/tests/CatenableSuite.scala rename to tests/src/test/scala/cats/tests/ChainSuite.scala index 28fdfa4b9e..ceef5b080e 100644 --- a/tests/src/test/scala/cats/tests/CatenableSuite.scala +++ b/tests/src/test/scala/cats/tests/ChainSuite.scala @@ -1,106 +1,106 @@ package cats package tests -import cats.data.Catenable +import cats.data.Chain import cats.kernel.laws.discipline.MonoidTests import cats.laws.discipline.{AlternativeTests, MonadTests, SerializableTests, TraverseTests} import cats.laws.discipline.arbitrary._ -class CatenableSuite extends CatsSuite { - checkAll("Catenable[Int]", AlternativeTests[Catenable].alternative[Int, Int, Int]) - checkAll("Alternative[Catenable]", SerializableTests.serializable(Alternative[Catenable])) +class ChainSuite extends CatsSuite { + checkAll("Chain[Int]", AlternativeTests[Chain].alternative[Int, Int, Int]) + checkAll("Alternative[Chain]", SerializableTests.serializable(Alternative[Chain])) - checkAll("Catenable[Int] with Option", TraverseTests[Catenable].traverse[Int, Int, Int, Set[Int], Option, Option]) - checkAll("Traverse[Catenable]", SerializableTests.serializable(Traverse[Catenable])) + checkAll("Chain[Int] with Option", TraverseTests[Chain].traverse[Int, Int, Int, Set[Int], Option, Option]) + checkAll("Traverse[Chain]", SerializableTests.serializable(Traverse[Chain])) - checkAll("Catenable[Int]", MonadTests[Catenable].monad[Int, Int, Int]) - checkAll("Monad[Catenable]", SerializableTests.serializable(Monad[Catenable])) + checkAll("Chain[Int]", MonadTests[Chain].monad[Int, Int, Int]) + checkAll("Monad[Chain]", SerializableTests.serializable(Monad[Chain])) - checkAll("Catenable[Int]", MonoidTests[Catenable[Int]].monoid) - checkAll("Monoid[Catenable]", SerializableTests.serializable(Monoid[Catenable[Int]])) + checkAll("Chain[Int]", MonoidTests[Chain[Int]].monoid) + checkAll("Monoid[Chain]", SerializableTests.serializable(Monoid[Chain[Int]])) test("show"){ - Show[Catenable[Int]].show(Catenable(1, 2, 3)) should === ("Catenable(1, 2, 3)") - Catenable.empty[Int].show should === ("Catenable()") - forAll { l: Catenable[String] => + Show[Chain[Int]].show(Chain(1, 2, 3)) should === ("Chain(1, 2, 3)") + Chain.empty[Int].show should === ("Chain()") + forAll { l: Chain[String] => l.show should === (l.toString) } } test("size is consistent with toList.size") { - forAll { (ci: Catenable[Int]) => + forAll { (ci: Chain[Int]) => ci.size should === (ci.toList.size) } } test("filterNot and then exists should always be false") { - forAll { (ci: Catenable[Int], f: Int => Boolean) => + forAll { (ci: Chain[Int], f: Int => Boolean) => ci.filterNot(f).exists(f) should === (false) } } test("filter and then forall should always be true") { - forAll { (ci: Catenable[Int], f: Int => Boolean) => + forAll { (ci: Chain[Int], f: Int => Boolean) => ci.filter(f).forall(f) should === (true) } } test("exists should be consistent with find + isDefined") { - forAll { (ci: Catenable[Int], f: Int => Boolean) => + forAll { (ci: Chain[Int], f: Int => Boolean) => ci.exists(f) should === (ci.find(f).isDefined) } } test("deleteFirst consistent with find") { - forAll { (ci: Catenable[Int], f: Int => Boolean) => + forAll { (ci: Chain[Int], f: Int => Boolean) => ci.find(f) should === (ci.deleteFirst(f).map(_._1)) } } test("filterNot element and then contains should be false") { - forAll { (ci: Catenable[Int], i: Int) => + forAll { (ci: Chain[Int], i: Int) => ci.filterNot(_ === i).contains(i) should === (false) } } test("Always nonempty after cons") { - forAll { (ci: Catenable[Int], i: Int) => + forAll { (ci: Chain[Int], i: Int) => (i +: ci).nonEmpty should === (true) } } test("fromSeq . toVector is id") { - forAll { (ci: Catenable[Int]) => - Catenable.fromSeq(ci.toVector) should === (ci) + forAll { (ci: Chain[Int]) => + Chain.fromSeq(ci.toVector) should === (ci) } } test("fromSeq . toList . iterator is id") { - forAll { (ci: Catenable[Int]) => - Catenable.fromSeq(ci.iterator.toList) should === (ci) + forAll { (ci: Chain[Int]) => + Chain.fromSeq(ci.iterator.toList) should === (ci) } } test("zipWith consistent with List#zip and then List#map") { - forAll { (a: Catenable[String], b: Catenable[Int], f: (String, Int) => Int) => + forAll { (a: Chain[String], b: Chain[Int], f: (String, Int) => Int) => a.zipWith(b)(f).toList should === (a.toList.zip(b.toList).map { case (x, y) => f(x, y) }) } } test("groupBy consistent with List#groupBy") { - forAll { (cs: Catenable[String], f: String => Int) => + forAll { (cs: Chain[String], f: String => Int) => cs.groupBy(f).map { case (k, v) => (k, v.toList) }.toMap should === (cs.toList.groupBy(f).toMap) } } test("reverse . reverse is id") { - forAll { (ci: Catenable[Int]) => + forAll { (ci: Chain[Int]) => ci.reverse.reverse should === (ci) } } test("reverse consistent with List#reverse") { - forAll { (ci: Catenable[Int]) => + forAll { (ci: Chain[Int]) => ci.reverse.toList should === (ci.toList.reverse) } } From f148b9af64beed047c8a19798c869d2b11af7c22 Mon Sep 17 00:00:00 2001 From: Luka Jacobowitz Date: Sun, 12 Aug 2018 12:50:59 +0200 Subject: [PATCH 14/19] More efficient implementations --- core/src/main/scala/cats/data/Chain.scala | 41 +++++++++++++++++++++-- 1 file changed, 39 insertions(+), 2 deletions(-) diff --git a/core/src/main/scala/cats/data/Chain.scala b/core/src/main/scala/cats/data/Chain.scala index 934e097f5c..2df9e3a58c 100644 --- a/core/src/main/scala/cats/data/Chain.scala +++ b/core/src/main/scala/cats/data/Chain.scala @@ -79,10 +79,19 @@ sealed abstract class Chain[+A] { final def flatMap[B](f: A => Chain[B]): Chain[B] = foldLeft(nil: Chain[B])((acc, a) => acc ++ f(a)) + /** Applies the supplied function to each element and returns a new Chain from the concatenated results */ + final def flatMapIterator[B](f: A => Chain[B]): Chain[B] = { + var result = empty[B] + val iter = iterator + while (iter.hasNext) { result = result ++ f(iter.next) } + result + } + /** Folds over the elements from left to right using the supplied initial value and function. */ final def foldLeft[B](z: B)(f: (B, A) => B): B = { var result = z - foreach(a => result = f(result, a)) + val iter = iterator + while (iter.hasNext) { result = f(result, iter.next) } result } @@ -122,6 +131,16 @@ sealed abstract class Chain[+A] { result } + final def findIterator(f: A => Boolean): Option[A] = { + val iter = iterator + var result: Option[A] = Option.empty[A] + while (iter.hasNext && result.isEmpty) { + val a = iter.next + if (f(a)) result = Some(a) + } + result + } + /** Check whether at least one element satisfies the predicate */ final def exists(f: A => Boolean): Boolean = { var result: Boolean = false @@ -182,6 +201,23 @@ sealed abstract class Chain[+A] { m } + final def groupByIterator[B](f: A => B)(implicit B: Order[B]): SortedMap[B, Chain[A]] = { + implicit val ordering: Ordering[B] = B.toOrdering + var m = SortedMap.empty[B, Chain[A]] + val iter = iterator + + while (iter.hasNext) { + val elem = iter.next + val k = f(elem) + + m.get(k) match { + case None => m += ((k, one(elem))); () + case Some(cat) => m = m.updated(k, cat :+ elem) + } + } + m + } + /** Reverses this `Chain` */ def reverse: Chain[A] = { Wrap(reverseIterator.toList) @@ -260,8 +296,9 @@ sealed abstract class Chain[+A] { /** Returns the number of elements in this structure */ final def length: Int = { + val iter = iterator var i: Int = 0 - foreach(_ => i += 1) + while(iter.hasNext) { i += 1; iter.next; } i } From 257a7a9ca4b211b65e8a4e3645197a2cbd3e98cd Mon Sep 17 00:00:00 2001 From: Luka Jacobowitz Date: Sun, 12 Aug 2018 20:48:54 +0200 Subject: [PATCH 15/19] Use Vector for reversing --- core/src/main/scala/cats/data/Chain.scala | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/core/src/main/scala/cats/data/Chain.scala b/core/src/main/scala/cats/data/Chain.scala index 2df9e3a58c..edb98c2ddf 100644 --- a/core/src/main/scala/cats/data/Chain.scala +++ b/core/src/main/scala/cats/data/Chain.scala @@ -219,9 +219,8 @@ sealed abstract class Chain[+A] { } /** Reverses this `Chain` */ - def reverse: Chain[A] = { - Wrap(reverseIterator.toList) - } + def reverse: Chain[A] = + fromSeq(reverseIterator.toVector) /** From 58caa4e7df01e48b40b78352cee24b188abeba44 Mon Sep 17 00:00:00 2001 From: Luka Jacobowitz Date: Mon, 13 Aug 2018 21:02:24 +0200 Subject: [PATCH 16/19] Remove redundant benchmarking methods --- core/src/main/scala/cats/data/Chain.scala | 30 +---------------------- 1 file changed, 1 insertion(+), 29 deletions(-) diff --git a/core/src/main/scala/cats/data/Chain.scala b/core/src/main/scala/cats/data/Chain.scala index edb98c2ddf..9724b6678b 100644 --- a/core/src/main/scala/cats/data/Chain.scala +++ b/core/src/main/scala/cats/data/Chain.scala @@ -75,12 +75,9 @@ sealed abstract class Chain[+A] { final def map[B](f: A => B): Chain[B] = fromSeq(iterator.map(f).toVector) - /** Applies the supplied function to each element and returns a new Chain from the concatenated results */ - final def flatMap[B](f: A => Chain[B]): Chain[B] = - foldLeft(nil: Chain[B])((acc, a) => acc ++ f(a)) /** Applies the supplied function to each element and returns a new Chain from the concatenated results */ - final def flatMapIterator[B](f: A => Chain[B]): Chain[B] = { + final def flatMap[B](f: A => Chain[B]): Chain[B] = { var result = empty[B] val iter = iterator while (iter.hasNext) { result = result ++ f(iter.next) } @@ -131,16 +128,6 @@ sealed abstract class Chain[+A] { result } - final def findIterator(f: A => Boolean): Option[A] = { - val iter = iterator - var result: Option[A] = Option.empty[A] - while (iter.hasNext && result.isEmpty) { - val a = iter.next - if (f(a)) result = Some(a) - } - result - } - /** Check whether at least one element satisfies the predicate */ final def exists(f: A => Boolean): Boolean = { var result: Boolean = false @@ -189,21 +176,6 @@ sealed abstract class Chain[+A] { final def groupBy[B](f: A => B)(implicit B: Order[B]): SortedMap[B, Chain[A]] = { implicit val ordering: Ordering[B] = B.toOrdering var m = SortedMap.empty[B, Chain[A]] - - foreach { elem => - val k = f(elem) - - m.get(k) match { - case None => m += ((k, one(elem))); () - case Some(cat) => m = m.updated(k, cat :+ elem) - } - } - m - } - - final def groupByIterator[B](f: A => B)(implicit B: Order[B]): SortedMap[B, Chain[A]] = { - implicit val ordering: Ordering[B] = B.toOrdering - var m = SortedMap.empty[B, Chain[A]] val iter = iterator while (iter.hasNext) { From 598c057d63fac1a5aba7ae82c13da1c8f43d31ef Mon Sep 17 00:00:00 2001 From: Luka Jacobowitz Date: Tue, 14 Aug 2018 13:58:14 +0200 Subject: [PATCH 17/19] Format scaladoc consistently --- core/src/main/scala/cats/data/Chain.scala | 120 ++++++++++++++++------ 1 file changed, 90 insertions(+), 30 deletions(-) diff --git a/core/src/main/scala/cats/data/Chain.scala b/core/src/main/scala/cats/data/Chain.scala index 9724b6678b..12d1549ef7 100644 --- a/core/src/main/scala/cats/data/Chain.scala +++ b/core/src/main/scala/cats/data/Chain.scala @@ -14,7 +14,9 @@ import scala.collection.immutable.SortedMap */ sealed abstract class Chain[+A] { - /** Returns the head and tail of this Chain if non empty, none otherwise. Amortized O(1). */ + /** + * Returns the head and tail of this Chain if non empty, none otherwise. Amortized O(1). + */ final def uncons: Option[(A, Chain[A])] = { var c: Chain[A] = this val rights = new collection.mutable.ArrayBuffer[Chain[A]] @@ -45,38 +47,62 @@ sealed abstract class Chain[+A] { result } - /** Returns true if there are no elements in this collection. */ - def isEmpty: Boolean + /** + * Returns true if there are no elements in this collection. + */ + final def isEmpty: Boolean - /** Returns false if there are no elements in this collection. */ - def nonEmpty: Boolean = !isEmpty + /** + * Returns false if there are no elements in this collection. + */ + final def nonEmpty: Boolean = !isEmpty - /** Concatenates this with `c` in O(1) runtime. */ - final def ++[A2 >: A](c: Chain[A2]): Chain[A2] = + /** + * Concatenates this with `c` in O(1) runtime. + */ + final def concat[A2 >: A](c: Chain[A2]): Chain[A2] = append(this, c) - /** Returns a new Chain consisting of `a` followed by this. O(1) runtime. */ + /** + * Alias for concat + */ + final def ++[A2 >: A](c: Chain[A2]): Chain[A2] = + concat(this, c) + + /** + * Returns a new Chain consisting of `a` followed by this. O(1) runtime. + */ final def cons[A2 >: A](a: A2): Chain[A2] = append(one(a), this) - /** Alias for [[cons]]. */ + /** + * Alias for [[cons]]. + */ final def +:[A2 >: A](a: A2): Chain[A2] = cons(a) - /** Returns a new Chain consisting of this followed by `a`. O(1) runtime. */ + /** + * Returns a new Chain consisting of this followed by `a`. O(1) runtime. + */ final def snoc[A2 >: A](a: A2): Chain[A2] = append(this, one(a)) - /** Alias for [[snoc]]. */ + /** + * Alias for [[snoc]]. + */ final def :+[A2 >: A](a: A2): Chain[A2] = snoc(a) - /** Applies the supplied function to each element and returns a new Chain. */ + /** + * Applies the supplied function to each element and returns a new Chain. + */ final def map[B](f: A => B): Chain[B] = fromSeq(iterator.map(f).toVector) - /** Applies the supplied function to each element and returns a new Chain from the concatenated results */ + /** + * Applies the supplied function to each element and returns a new Chain from the concatenated results + */ final def flatMap[B](f: A => Chain[B]): Chain[B] = { var result = empty[B] val iter = iterator @@ -84,7 +110,9 @@ sealed abstract class Chain[+A] { result } - /** Folds over the elements from left to right using the supplied initial value and function. */ + /** + * Folds over the elements from left to right using the supplied initial value and function. + */ final def foldLeft[B](z: B)(f: (B, A) => B): B = { var result = z val iter = iterator @@ -92,7 +120,9 @@ sealed abstract class Chain[+A] { result } - /** Folds over the elements from right to left using the supplied initial value and function. */ + /** + * Folds over the elements from right to left using the supplied initial value and function. + */ final def foldRight[B](z: B)(f: (A, B) => B): B = { var result = z val iter = reverseIterator @@ -100,7 +130,9 @@ sealed abstract class Chain[+A] { result } - /** Collect `B` from this for which `f` is defined */ + /** + * Collect `B` from this for which `f` is defined + */ final def collect[B](pf: PartialFunction[A, B]): Chain[B] = foldLeft(Chain.nil: Chain[B]) { (acc, a) => // trick from TraversableOnce, used to avoid calling both isDefined and apply (or calling lift) @@ -109,15 +141,21 @@ sealed abstract class Chain[+A] { else acc } - /** Remove elements not matching the predicate */ + /** + * Remove elements not matching the predicate + */ final def filter(f: A => Boolean): Chain[A] = collect { case a if f(a) => a } - /** Remove elements matching the predicate */ + /** + * Remove elements matching the predicate + */ final def filterNot(f: A => Boolean): Chain[A] = filter(a => !f(a)) - /** Find the first element matching the predicate, if one exists */ + /** + * Find the first element matching the predicate, if one exists + */ final def find(f: A => Boolean): Option[A] = { var result: Option[A] = Option.empty[A] foreachUntil { a => @@ -128,7 +166,9 @@ sealed abstract class Chain[+A] { result } - /** Check whether at least one element satisfies the predicate */ + /** + * Check whether at least one element satisfies the predicate + */ final def exists(f: A => Boolean): Boolean = { var result: Boolean = false foreachUntil { a => @@ -139,7 +179,9 @@ sealed abstract class Chain[+A] { result } - /** Check whether all elements satisfy the predicate */ + /** + * Check whether all elements satisfy the predicate + */ final def forall(f: A => Boolean): Boolean = { var result: Boolean = true foreachUntil { a => @@ -150,11 +192,15 @@ sealed abstract class Chain[+A] { result } - /** Check whether an element is in this structure */ + /** + * Check whether an element is in this structure + */ final def contains[AA >: A](a: AA)(implicit A: Eq[AA]): Boolean = exists(A.eqv(a, _)) - /** Zips this `Chain` with another `Chain` and applies a function for each pair of elements. */ + /** + * Zips this `Chain` with another `Chain` and applies a function for each pair of elements. + */ final def zipWith[B, C](other: Chain[B])(f: (A, B) => C): Chain[C] = if (this.isEmpty || other.isEmpty) Chain.Empty else { @@ -190,7 +236,9 @@ sealed abstract class Chain[+A] { m } - /** Reverses this `Chain` */ + /** + * Reverses this `Chain` + */ def reverse: Chain[A] = fromSeq(reverseIterator.toVector) @@ -213,10 +261,14 @@ sealed abstract class Chain[+A] { go(this, Chain.nil) } - /** Applies the supplied function to each element, left to right. */ + /** + * Applies the supplied function to each element, left to right. + */ private final def foreach(f: A => Unit): Unit = foreachUntil { a => f(a); false } - /** Applies the supplied function to each element, left to right, but stops when true is returned */ + /** + * Applies the supplied function to each element, left to right, but stops when true is returned + */ // scalastyle:off null return cyclomatic.complexity private final def foreachUntil(f: A => Boolean): Unit = { var c: Chain[A] = this @@ -265,7 +317,9 @@ sealed abstract class Chain[+A] { case _ => new ChainReverseIterator[A](this) } - /** Returns the number of elements in this structure */ + /** + * Returns the number of elements in this structure + */ final def length: Int = { val iter = iterator var i: Int = 0 @@ -273,15 +327,21 @@ sealed abstract class Chain[+A] { i } - /** Alias for length */ + /** + * Alias for length + */ final def size: Int = length - /** Converts to a list. */ + /** + * Converts to a list. + */ final def toList: List[A] = iterator.toList - /** Converts to a vector. */ + /** + * Converts to a vector. + */ final def toVector: Vector[A] = iterator.toVector From 76174a74357f4ba916e927da28ac79c77498323a Mon Sep 17 00:00:00 2001 From: Luka Jacobowitz Date: Tue, 14 Aug 2018 14:27:53 +0200 Subject: [PATCH 18/19] Rename snoc and cons to append and prepend for consistency --- core/src/main/scala/cats/data/Chain.scala | 32 +++++++++---------- .../cats/laws/discipline/Arbitrary.scala | 2 +- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/core/src/main/scala/cats/data/Chain.scala b/core/src/main/scala/cats/data/Chain.scala index 12d1549ef7..6dc6301fd7 100644 --- a/core/src/main/scala/cats/data/Chain.scala +++ b/core/src/main/scala/cats/data/Chain.scala @@ -50,7 +50,7 @@ sealed abstract class Chain[+A] { /** * Returns true if there are no elements in this collection. */ - final def isEmpty: Boolean + def isEmpty: Boolean /** * Returns false if there are no elements in this collection. @@ -61,37 +61,37 @@ sealed abstract class Chain[+A] { * Concatenates this with `c` in O(1) runtime. */ final def concat[A2 >: A](c: Chain[A2]): Chain[A2] = - append(this, c) + Chain.concat(this, c) /** * Alias for concat */ final def ++[A2 >: A](c: Chain[A2]): Chain[A2] = - concat(this, c) + concat(c) /** * Returns a new Chain consisting of `a` followed by this. O(1) runtime. */ - final def cons[A2 >: A](a: A2): Chain[A2] = - append(one(a), this) + final def prepend[A2 >: A](a: A2): Chain[A2] = + Chain.concat(one(a), this) /** - * Alias for [[cons]]. + * Alias for [[prepend]]. */ final def +:[A2 >: A](a: A2): Chain[A2] = - cons(a) + prepend(a) /** * Returns a new Chain consisting of this followed by `a`. O(1) runtime. */ - final def snoc[A2 >: A](a: A2): Chain[A2] = - append(this, one(a)) + final def append[A2 >: A](a: A2): Chain[A2] = + Chain.concat(this, one(a)) /** - * Alias for [[snoc]]. + * Alias for [[append]]. */ final def :+[A2 >: A](a: A2): Chain[A2] = - snoc(a) + append(a) /** * Applies the supplied function to each element and returns a new Chain. @@ -374,7 +374,7 @@ object Chain extends ChainInstances { private[data] final case class Append[A](left: Chain[A], right: Chain[A]) extends Chain[A] { def isEmpty: Boolean = - false // b/c `append` constructor doesn't allow either branch to be empty + false // b/c `concat` constructor doesn't allow either branch to be empty } private[data] final case class Wrap[A](seq: Seq[A]) extends Chain[A] { override def isEmpty: Boolean = @@ -389,8 +389,8 @@ object Chain extends ChainInstances { /** Creates a Chain of 1 element. */ def one[A](a: A): Chain[A] = Singleton(a) - /** Appends two Chains. */ - def append[A](c: Chain[A], c2: Chain[A]): Chain[A] = + /** Concatenates two Chains. */ + def concat[A](c: Chain[A], c2: Chain[A]): Chain[A] = if (c.isEmpty) c2 else if (c2.isEmpty) c else Append(c, c2) @@ -511,7 +511,7 @@ object Chain extends ChainInstances { private[data] sealed abstract class ChainInstances { implicit def catsDataMonoidForChain[A]: Monoid[Chain[A]] = new Monoid[Chain[A]] { def empty: Chain[A] = Chain.nil - def combine(c: Chain[A], c2: Chain[A]): Chain[A] = Chain.append(c, c2) + def combine(c: Chain[A], c2: Chain[A]): Chain[A] = Chain.concat(c, c2) } implicit val catsDataInstancesForChain: Traverse[Chain] with Alternative[Chain] with Monad[Chain] = @@ -535,7 +535,7 @@ private[data] sealed abstract class ChainInstances { G.map2(f(a), gcatb)(_ +: _) } def empty[A]: Chain[A] = Chain.nil - def combineK[A](c: Chain[A], c2: Chain[A]): Chain[A] = Chain.append(c, c2) + def combineK[A](c: Chain[A], c2: Chain[A]): Chain[A] = Chain.concat(c, c2) def pure[A](a: A): Chain[A] = Chain.one(a) def flatMap[A, B](fa: Chain[A])(f: A => Chain[B]): Chain[B] = fa.flatMap(f) diff --git a/laws/src/main/scala/cats/laws/discipline/Arbitrary.scala b/laws/src/main/scala/cats/laws/discipline/Arbitrary.scala index 5f4452b755..82630e63a4 100644 --- a/laws/src/main/scala/cats/laws/discipline/Arbitrary.scala +++ b/laws/src/main/scala/cats/laws/discipline/Arbitrary.scala @@ -281,7 +281,7 @@ object arbitrary extends ArbitraryInstances0 { case 0 => Gen.const(Chain.nil) case 1 => A.arbitrary.map(Chain.one) case 2 => A.arbitrary.flatMap(a1 => A.arbitrary.flatMap(a2 => - Chain.append(Chain.one(a1), Chain.one(a2)))) + Chain.concat(Chain.one(a1), Chain.one(a2)))) case n => Chain.fromSeq(Range.apply(0, n)).foldLeft(Gen.const(Chain.empty[A])) { (gen, _) => gen.flatMap(cat => A.arbitrary.map(a => cat :+ a)) } From bc66361dd5beee411a76a437f98de66adbb99d83 Mon Sep 17 00:00:00 2001 From: Luka Jacobowitz Date: Wed, 15 Aug 2018 10:00:13 +0200 Subject: [PATCH 19/19] Add proper Eq, PartialOrder, Order and Coflatmap instances --- core/src/main/scala/cats/data/Chain.scala | 90 +++++++++++++++++-- .../cats/laws/discipline/Arbitrary.scala | 3 + .../test/scala/cats/tests/ChainSuite.scala | 10 ++- 3 files changed, 95 insertions(+), 8 deletions(-) diff --git a/core/src/main/scala/cats/data/Chain.scala b/core/src/main/scala/cats/data/Chain.scala index 6dc6301fd7..99c95eaa89 100644 --- a/core/src/main/scala/cats/data/Chain.scala +++ b/core/src/main/scala/cats/data/Chain.scala @@ -1,11 +1,11 @@ package cats package data -import cats.implicits._ import Chain._ import scala.annotation.tailrec import scala.collection.immutable.SortedMap +import scala.collection.mutable.ListBuffer /** * Trivial catenable sequence. Supports O(1) append, and (amortized) @@ -345,6 +345,27 @@ sealed abstract class Chain[+A] { final def toVector: Vector[A] = iterator.toVector + /** + * Typesafe equality operator. + * + * This method is similar to == except that it only allows two + * Chain[A] values to be compared to each other, and uses + * equality provided by Eq[_] instances, rather than using the + * universal equality provided by .equals. + */ + def ===[AA >: A](that: Chain[AA])(implicit A: Eq[AA]): Boolean = + (this eq that) || { + val iterX = iterator + val iterY = that.iterator + while (iterX.hasNext && iterY.hasNext) { + // scalastyle:off return + if (!A.eqv(iterX.next, iterY.next)) return false + // scalastyle:on return + } + + iterX.hasNext == iterY.hasNext + } + def show[AA >: A](implicit AA: Show[AA]): String = { val builder = new StringBuilder("Chain(") var first = true @@ -508,14 +529,15 @@ object Chain extends ChainInstances { // scalastyle:on null } -private[data] sealed abstract class ChainInstances { +private[data] sealed abstract class ChainInstances extends ChainInstances1 { implicit def catsDataMonoidForChain[A]: Monoid[Chain[A]] = new Monoid[Chain[A]] { def empty: Chain[A] = Chain.nil def combine(c: Chain[A], c2: Chain[A]): Chain[A] = Chain.concat(c, c2) } - implicit val catsDataInstancesForChain: Traverse[Chain] with Alternative[Chain] with Monad[Chain] = - new Traverse[Chain] with Alternative[Chain] with Monad[Chain] { + implicit val catsDataInstancesForChain: Traverse[Chain] with Alternative[Chain] + with Monad[Chain] with CoflatMap[Chain] = + new Traverse[Chain] with Alternative[Chain] with Monad[Chain] with CoflatMap[Chain] { def foldLeft[A, B](fa: Chain[A], b: B)(f: (B, A) => B): B = fa.foldLeft(b)(f) def foldRight[A, B](fa: Chain[A], lb: Eval[B])(f: (A, Eval[B]) => Eval[B]): Eval[B] = @@ -530,6 +552,16 @@ private[data] sealed abstract class ChainInstances { override def forall[A](fa: Chain[A])(p: A => Boolean): Boolean = fa.forall(p) override def find[A](fa: Chain[A])(f: A => Boolean): Option[A] = fa.find(f) + def coflatMap[A, B](fa: Chain[A])(f: Chain[A] => B): Chain[B] = { + @tailrec def go(as: Chain[A], res: ListBuffer[B]): Chain[B] = + as.uncons match { + case Some((h, t)) => go(t, res += f(t)) + case None => Chain.fromSeq(res.result()) + } + + go(fa, ListBuffer.empty) + } + def traverse[G[_], A, B](fa: Chain[A])(f: A => G[B])(implicit G: Applicative[G]): G[Chain[B]] = fa.foldRight[G[Chain[B]]](G.pure(nil)) { (a, gcatb) => G.map2(f(a), gcatb)(_ +: _) @@ -566,9 +598,55 @@ private[data] sealed abstract class ChainInstances { implicit def catsDataShowForChain[A](implicit A: Show[A]): Show[Chain[A]] = Show.show[Chain[A]](_.show) + implicit def catsDataOrderForChain[A](implicit A0: Order[A]): Order[Chain[A]] = + new Order[Chain[A]] with ChainPartialOrder[A] { + implicit def A: PartialOrder[A] = A0 + def compare(x: Chain[A], y: Chain[A]): Int = if (x eq y) 0 else { + val iterX = x.iterator + val iterY = y.iterator + while (iterX.hasNext && iterY.hasNext) { + val n = A0.compare(iterX.next, iterY.next) + // scalastyle:off return + if (n != 0) return n + // scalastyle:on return + } + + if (iterX.hasNext) 1 + else if (iterY.hasNext) -1 + else 0 + } + } + +} + +private[data] sealed abstract class ChainInstances1 extends ChainInstances2 { + implicit def catsDataPartialOrderForChain[A](implicit A0: PartialOrder[A]): PartialOrder[Chain[A]] = + new ChainPartialOrder[A] { implicit def A: PartialOrder[A] = A0 } +} + +private[data] sealed abstract class ChainInstances2 { implicit def catsDataEqForChain[A](implicit A: Eq[A]): Eq[Chain[A]] = new Eq[Chain[A]] { - def eqv(x: Chain[A], y: Chain[A]): Boolean = - (x eq y) || x.toList === y.toList + def eqv(x: Chain[A], y: Chain[A]): Boolean = x === y + } +} + +private[data] trait ChainPartialOrder[A] extends PartialOrder[Chain[A]] { + implicit def A: PartialOrder[A] + + override def partialCompare(x: Chain[A], y: Chain[A]): Double = if (x eq y) 0.0 else { + val iterX = x.iterator + val iterY = y.iterator + while (iterX.hasNext && iterY.hasNext) { + val n = A.partialCompare(iterX.next, iterY.next) + // scalastyle:off return + if (n != 0.0) return n + // scalastyle:on return + } + + if (iterX.hasNext) 1.0 + else if (iterY.hasNext) -1.0 + else 0.0 } + override def eqv(x: Chain[A], y: Chain[A]): Boolean = x === y } diff --git a/laws/src/main/scala/cats/laws/discipline/Arbitrary.scala b/laws/src/main/scala/cats/laws/discipline/Arbitrary.scala index 82630e63a4..fcb6d16ac7 100644 --- a/laws/src/main/scala/cats/laws/discipline/Arbitrary.scala +++ b/laws/src/main/scala/cats/laws/discipline/Arbitrary.scala @@ -287,6 +287,9 @@ object arbitrary extends ArbitraryInstances0 { } }) + implicit def catsLawsCogenForChain[A](implicit A: Cogen[A]): Cogen[Chain[A]] = + Cogen[List[A]].contramap(_.toList) + } private[discipline] sealed trait ArbitraryInstances0 { diff --git a/tests/src/test/scala/cats/tests/ChainSuite.scala b/tests/src/test/scala/cats/tests/ChainSuite.scala index ceef5b080e..a83a3d77d1 100644 --- a/tests/src/test/scala/cats/tests/ChainSuite.scala +++ b/tests/src/test/scala/cats/tests/ChainSuite.scala @@ -2,8 +2,8 @@ package cats package tests import cats.data.Chain -import cats.kernel.laws.discipline.MonoidTests -import cats.laws.discipline.{AlternativeTests, MonadTests, SerializableTests, TraverseTests} +import cats.kernel.laws.discipline.{MonoidTests, OrderTests} +import cats.laws.discipline.{AlternativeTests, CoflatMapTests, MonadTests, SerializableTests, TraverseTests} import cats.laws.discipline.arbitrary._ class ChainSuite extends CatsSuite { @@ -16,9 +16,15 @@ class ChainSuite extends CatsSuite { checkAll("Chain[Int]", MonadTests[Chain].monad[Int, Int, Int]) checkAll("Monad[Chain]", SerializableTests.serializable(Monad[Chain])) + checkAll("Chain[Int]", CoflatMapTests[Chain].coflatMap[Int, Int, Int]) + checkAll("Coflatmap[Chain]", SerializableTests.serializable(CoflatMap[Chain])) + checkAll("Chain[Int]", MonoidTests[Chain[Int]].monoid) checkAll("Monoid[Chain]", SerializableTests.serializable(Monoid[Chain[Int]])) + checkAll("Chain[Int]", OrderTests[Chain[Int]].order) + checkAll("Order[Chain]", SerializableTests.serializable(Order[Chain[Int]])) + test("show"){ Show[Chain[Int]].show(Chain(1, 2, 3)) should === ("Chain(1, 2, 3)") Chain.empty[Int].show should === ("Chain()")