From 21aa369893fb403601df77151028d202f86276a2 Mon Sep 17 00:00:00 2001 From: Luka Jacobowitz Date: Wed, 15 Aug 2018 17:59:21 +0200 Subject: [PATCH] Add Chain (#2371) * Add Catenable * Add law tests and simple arbitrary and eq instances * More tests * Add benchmarks * Add chain benchmarks * Add Iterator * More chain benchmarks * Add Paul and Pavel to Authors and change COPYING to Cats Contributors * More Tests * Add reverse, groupBy and zipWith * Add Collection Wrapping optimization * Add traverse and foldRight implementations that don't convert to List * Rename to Chain; add reverseIterator * More efficient implementations * Use Vector for reversing * Remove redundant benchmarking methods * Format scaladoc consistently * Rename snoc and cons to append and prepend for consistency * Add proper Eq, PartialOrder, Order and Coflatmap instances --- AUTHORS.md | 2 + COPYING | 2 +- .../main/scala/cats/bench/ChainBench.scala | 79 +++ .../cats/bench/CollectionMonoidBench.scala | 24 + build.sbt | 5 +- core/src/main/scala/cats/data/Chain.scala | 652 ++++++++++++++++++ .../cats/laws/discipline/Arbitrary.scala | 14 + .../test/scala/cats/tests/ChainSuite.scala | 113 +++ 8 files changed, 889 insertions(+), 2 deletions(-) create mode 100644 bench/src/main/scala/cats/bench/ChainBench.scala create mode 100644 bench/src/main/scala/cats/bench/CollectionMonoidBench.scala create mode 100644 core/src/main/scala/cats/data/Chain.scala create mode 100644 tests/src/test/scala/cats/tests/ChainSuite.scala 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 diff --git a/bench/src/main/scala/cats/bench/ChainBench.scala b/bench/src/main/scala/cats/bench/ChainBench.scala new file mode 100644 index 0000000000..a8e64b3409 --- /dev/null +++ b/bench/src/main/scala/cats/bench/ChainBench.scala @@ -0,0 +1,79 @@ +package cats.bench + +import cats.data.Chain +import fs2.Catenable +import chain.{Chain => OldChain} +import org.openjdk.jmh.annotations.{Benchmark, Scope, State} + +@State(Scope.Thread) +class ChainBench { + + private val smallChain = Chain(1, 2, 3, 4, 5) + 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 smallOldChain = OldChain(smallList) + + 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 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 mapSmallVector: Vector[Int] = smallVector.map(_ + 1) + @Benchmark def mapSmallList: List[Int] = smallList.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 mapLargeVector: Vector[Int] = largeVector.map(_ + 1) + @Benchmark def mapLargeList: List[Int] = largeList.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 foldLeftSmallVector: Int = smallVector.foldLeft(0)(_ + _) + @Benchmark def foldLeftSmallList: Int = smallList.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 foldLeftLargeVector: Int = largeVector.foldLeft(0)(_ + _) + @Benchmark def foldLeftLargeList: Int = largeList.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 consSmallVector: Vector[Int] = 0 +: smallVector + @Benchmark def consSmallList: List[Int] = 0 +: smallList + @Benchmark def consSmallOldChain: OldChain[Int] = 0 +: smallOldChain + + @Benchmark def consLargeChain: Chain[Int] = 0 +: largeChain + @Benchmark def consLargeCatenable: Catenable[Int] = 0 +: largeCatenable + @Benchmark def consLargeVector: Vector[Int] = 0 +: largeVector + @Benchmark def consLargeList: List[Int] = 0 +: largeList + @Benchmark def consLargeOldChain: OldChain[Int] = 0 +: largeOldChain + + @Benchmark def createTinyChain: Chain[Int] = Chain(1) + @Benchmark def createTinyCatenable: Catenable[Int] = Catenable(1) + @Benchmark def createTinyVector: Vector[Int] = Vector(1) + @Benchmark def createTinyList: List[Int] = List(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 createSmallVector: Vector[Int] = Vector(1, 2, 3, 4, 5) + @Benchmark def createSmallList: List[Int] = List(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 new file mode 100644 index 0000000000..81d4ec1970 --- /dev/null +++ b/bench/src/main/scala/cats/bench/CollectionMonoidBench.scala @@ -0,0 +1,24 @@ +package cats.bench + +import cats.Monoid +import cats.data.Chain +import cats.implicits._ +import chain.{Chain => OldChain} +import org.openjdk.jmh.annotations.{Benchmark, Scope, State} + +@State(Scope.Thread) +class CollectionMonoidBench { + + private val largeList = (0 to 1000000).toList + + implicit def monoidOldChain[A]: Monoid[OldChain[A]] = new Monoid[OldChain[A]] { + def empty: OldChain[A] = OldChain.empty[A] + + def combine(x: OldChain[A], y: OldChain[A]): OldChain[A] = x ++ y + } + + @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 accumulateOldChain: OldChain[Int] = largeList.foldMap(OldChain.single) +} diff --git a/build.sbt b/build.sbt index 588726cc24..fee016a7fa 100644 --- a/build.sbt +++ b/build.sbt @@ -491,7 +491,10 @@ 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", + "co.fs2" %% "fs2-core" % "0.10.4" + )) .enablePlugins(JmhPlugin) // cats-js is JS-only diff --git a/core/src/main/scala/cats/data/Chain.scala b/core/src/main/scala/cats/data/Chain.scala new file mode 100644 index 0000000000..99c95eaa89 --- /dev/null +++ b/core/src/main/scala/cats/data/Chain.scala @@ -0,0 +1,652 @@ +package cats +package data + +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) + * O(1) `uncons`, such that walking the sequence via N successive `uncons` + * steps takes O(N). + */ +sealed abstract class Chain[+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, Chain[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 + } else { + c = rights.last + rights.trimEnd(1) + } + } + } + // scalastyle:on null + result + } + + /** + * Returns true if there are no elements in this collection. + */ + def isEmpty: Boolean + + /** + * 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 concat[A2 >: A](c: Chain[A2]): Chain[A2] = + Chain.concat(this, c) + + /** + * Alias for concat + */ + final def ++[A2 >: A](c: Chain[A2]): Chain[A2] = + concat(c) + + /** + * Returns a new Chain consisting of `a` followed by this. O(1) runtime. + */ + final def prepend[A2 >: A](a: A2): Chain[A2] = + Chain.concat(one(a), this) + + /** + * Alias for [[prepend]]. + */ + final def +:[A2 >: A](a: A2): Chain[A2] = + prepend(a) + + /** + * Returns a new Chain consisting of this followed by `a`. O(1) runtime. + */ + final def append[A2 >: A](a: A2): Chain[A2] = + Chain.concat(this, one(a)) + + /** + * Alias for [[append]]. + */ + final def :+[A2 >: A](a: A2): Chain[A2] = + append(a) + + /** + * 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 + */ + 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) } + 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 + val iter = iterator + while (iter.hasNext) { result = f(result, iter.next) } + 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 = { + var result = z + 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]): 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] + else acc + } + + /** + * 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 + */ + final def filterNot(f: A => Boolean): Chain[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 + */ + final 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 + */ + 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 `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: Chain[C] = Chain.one(f(iterA.next(), iterB.next())) + + while (iterA.hasNext && iterB.hasNext) { + result = result :+ f(iterA.next(), iterB.next()) + } + result + } + + /** + * 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, 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] = + fromSeq(reverseIterator.toVector) + + + /** + * 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, Chain[A])] = { + @tailrec + 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) + else Some((a, acc ++ tail)) + + case None => None + } + go(this, Chain.nil) + } + + /** + * 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 + */ + // scalastyle:off null return cyclomatic.complexity + private final def foreachUntil(f: A => Boolean): Unit = { + var c: Chain[A] = this + val rights = new collection.mutable.ArrayBuffer[Chain[A]] + + while (c ne null) { + c match { + case Singleton(a) => + val b = f(a) + if (b) return (); + c = + if (rights.isEmpty) Empty + 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 cyclomatic.complexity + + + final def iterator: Iterator[A] = this match { + case Wrap(seq) => seq.iterator + 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 + */ + final def length: Int = { + val iter = iterator + var i: Int = 0 + while(iter.hasNext) { i += 1; iter.next; } + i + } + + /** + * Alias for length + */ + final def size: Int = length + + + /** + * Converts to a list. + */ + final def toList: List[A] = + iterator.toList + + /** + * Converts to a vector. + */ + 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 + + foreach { a => + if (first) { builder ++= AA.show(a); first = false } + else builder ++= ", " + AA.show(a) + () + } + builder += ')' + builder.result + } + + override def toString: String = show(Show.show[A](_.toString)) +} + +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 Chain[Nothing] { + def isEmpty: Boolean = true + } + private[data] final case class Singleton[A](a: A) extends Chain[A] { + def isEmpty: Boolean = false + } + private[data] final case class Append[A](left: Chain[A], right: Chain[A]) + extends Chain[A] { + def isEmpty: Boolean = + 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 = + false // b/c `fromSeq` constructor doesn't allow either branch to be empty + } + + /** Empty Chain. */ + val nil: Chain[Nothing] = Empty + + def empty[A]: Chain[A] = nil + + /** Creates a Chain of 1 element. */ + def one[A](a: A): Chain[A] = Singleton(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) + + /** 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 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) one(w.head) + else { + val arr: Array[A] = w.array + var c: Chain[A] = one(arr.last) + var idx = arr.size - 2 + while (idx >= 0) { + c = Append(one(arr(idx)), c) + idx -= 1 + } + c + } + case _ => fromSeq(as) + } + + // scalastyle:off null + 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) + + 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 (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 + } + } + // 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 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] 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] = + Eval.defer(fa.foldRight(lb) { (a, lb) => + Eval.defer(f(a, lb)) + }) + + 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 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)(_ +: _) + } + def empty[A]: Chain[A] = Chain.nil + 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) + 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 { + 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 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 === 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 c7d077dfd6..fcb6d16ac7 100644 --- a/laws/src/main/scala/cats/laws/discipline/Arbitrary.scala +++ b/laws/src/main/scala/cats/laws/discipline/Arbitrary.scala @@ -276,6 +276,20 @@ 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 catsLawsArbitraryForChain[A](implicit A: Arbitrary[A]): Arbitrary[Chain[A]] = + Arbitrary(Gen.sized { + case 0 => Gen.const(Chain.nil) + case 1 => A.arbitrary.map(Chain.one) + case 2 => A.arbitrary.flatMap(a1 => A.arbitrary.flatMap(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)) + } + }) + + 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 new file mode 100644 index 0000000000..a83a3d77d1 --- /dev/null +++ b/tests/src/test/scala/cats/tests/ChainSuite.scala @@ -0,0 +1,113 @@ +package cats +package tests + +import cats.data.Chain +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 { + checkAll("Chain[Int]", AlternativeTests[Chain].alternative[Int, Int, Int]) + checkAll("Alternative[Chain]", SerializableTests.serializable(Alternative[Chain])) + + checkAll("Chain[Int] with Option", TraverseTests[Chain].traverse[Int, Int, Int, Set[Int], Option, Option]) + checkAll("Traverse[Chain]", SerializableTests.serializable(Traverse[Chain])) + + 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()") + forAll { l: Chain[String] => + l.show should === (l.toString) + } + } + + test("size is consistent with toList.size") { + forAll { (ci: Chain[Int]) => + ci.size should === (ci.toList.size) + } + } + + test("filterNot and then exists should always be false") { + 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: Chain[Int], f: Int => Boolean) => + ci.filter(f).forall(f) should === (true) + } + } + + test("exists should be consistent with find + isDefined") { + forAll { (ci: Chain[Int], f: Int => Boolean) => + ci.exists(f) should === (ci.find(f).isDefined) + } + } + + test("deleteFirst consistent with find") { + 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: Chain[Int], i: Int) => + ci.filterNot(_ === i).contains(i) should === (false) + } + } + + test("Always nonempty after cons") { + forAll { (ci: Chain[Int], i: Int) => + (i +: ci).nonEmpty should === (true) + } + } + + test("fromSeq . toVector is id") { + forAll { (ci: Chain[Int]) => + Chain.fromSeq(ci.toVector) should === (ci) + } + } + + test("fromSeq . toList . iterator is id") { + forAll { (ci: Chain[Int]) => + Chain.fromSeq(ci.iterator.toList) should === (ci) + } + } + + test("zipWith consistent with List#zip and then List#map") { + 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: 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: Chain[Int]) => + ci.reverse.reverse should === (ci) + } + } + + test("reverse consistent with List#reverse") { + forAll { (ci: Chain[Int]) => + ci.reverse.toList should === (ci.toList.reverse) + } + } +}