-
Notifications
You must be signed in to change notification settings - Fork 64
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* implement traverse derivation * use TraverseOrMk to solve the mixed order type param problem * disambiguate TraverseSuite * add missing traverse references to package object derived * implemente stacksafe version of traverse * write tests for the implemented traverse derivation * attribute Baccata and mention that the default implementation could be removed in the future * remove unnecessary Eval.later call * hide ambiguous implicits from the MkTraverse companion * both HCons and CCons were resolved by the 2.13 typer for an arbitrary case class * test default fold methods of traverse and minor style changes * use existing Endo instances from cats in traverse * implement tests of default traverse.fold methods * rewrite traverse such that it derives both safeTraverse and fold methods
- Loading branch information
1 parent
533a5e1
commit 2528d17
Showing
5 changed files
with
346 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,166 @@ | ||
package cats.derived | ||
|
||
import cats.syntax.all._ | ||
import cats.{Applicative, Eval, Now, Traverse} | ||
import shapeless._ | ||
|
||
import scala.annotation.implicitNotFound | ||
|
||
/** | ||
* Based on the `MkFoldable` implementation. | ||
*/ | ||
@implicitNotFound("Could not derive an instance of Traverse[${F}]") | ||
trait MkTraverse[F[_]] extends Traverse[F] { | ||
|
||
def safeTraverse[G[_] : Applicative, A, B](fa: F[A])(f: A => Eval[G[B]]): Eval[G[F[B]]] | ||
|
||
def safeFoldLeft[A, B](fa: F[A], b: B)(f: (B, A) => Eval[B]): Eval[B] | ||
|
||
def foldRight[A, B](fa: F[A], lb: Eval[B])(f: (A, Eval[B]) => Eval[B]): Eval[B] | ||
|
||
def traverse[G[_] : Applicative, A, B](fa: F[A])(f: A => G[B]): G[F[B]] = | ||
safeTraverse(fa)(a => Now(f(a))).value | ||
|
||
def foldLeft[A, B](fa: F[A], b: B)(f: (B, A) => B): B = | ||
safeFoldLeft(fa, b) { (b, a) => Now(f(b, a)) }.value | ||
|
||
} | ||
|
||
object MkTraverse extends MkTraverseDerivation { | ||
def apply[F[_]](implicit mff: MkTraverse[F]): MkTraverse[F] = mff | ||
} | ||
|
||
trait MkTraverseDerivation extends MkTraverse0 { | ||
implicit val mkTraverseId: MkTraverse[shapeless.Id] = new MkTraverse[shapeless.Id] { | ||
def safeTraverse[G[_] : Applicative, A, B](fa: Id[A])(f: A => Eval[G[B]]): Eval[G[Id[B]]] = f(fa) | ||
|
||
def foldRight[A, B](fa: A, lb: Eval[B])(f: (A, Eval[B]) => Eval[B]): Eval[B] = f(fa, lb) | ||
|
||
def safeFoldLeft[A, B](fa: A, b: B)(f: (B, A) => Eval[B]): Eval[B] = Now(f(b, fa).value) | ||
|
||
} | ||
|
||
implicit def mkTraverseConst[T]: MkTraverse[Const[T]#λ] = new MkTraverse[Const[T]#λ] { | ||
override def safeTraverse[G[_] : Applicative, A, B](fa: T)(f: A => Eval[G[B]]): Eval[G[T]] = | ||
Now(fa.pure[G]) | ||
|
||
def foldRight[A, B](fa: T, lb: Eval[B])(f: (A, Eval[B]) => Eval[B]): Eval[B] = lb | ||
|
||
def safeFoldLeft[A, B](fa: T, b: B)(f: (B, A) => Eval[B]): Eval[B] = Now(b) | ||
} | ||
} | ||
|
||
private[derived] trait MkTraverse0 extends MkTraverse1 { | ||
// Induction step for products | ||
implicit def mkTraverseHcons[F[_]](implicit ihc: IsHCons1[F, TraverseOrMk, MkTraverse]): MkTraverse[F] = | ||
new MkTraverse[F] { | ||
override def safeTraverse[G[_] : Applicative, A, B](fa: F[A])(f: A => Eval[G[B]]): Eval[G[F[B]]] = { | ||
for { | ||
ht <- Now(ihc.unpack(fa)) | ||
th <- ihc.fh.unify.safeTraverse(ht._1)(f) | ||
tt <- ihc.ft.safeTraverse(ht._2)(f) | ||
} yield (th, tt).mapN(ihc.pack(_, _)) | ||
} | ||
|
||
def foldRight[A, B](fa: F[A], lb: Eval[B])(f: (A, Eval[B]) => Eval[B]): Eval[B] = { | ||
import ihc._ | ||
val (hd, tl) = unpack(fa) | ||
for { | ||
t <- ft.foldRight(tl, lb)(f) | ||
h <- fh.unify.foldRight(hd, Now(t))(f) | ||
} yield h | ||
} | ||
|
||
def safeFoldLeft[A, B](fa: F[A], b: B)(f: (B, A) => Eval[B]): Eval[B] = { | ||
import ihc._ | ||
val (hd, tl) = unpack(fa) | ||
for { | ||
h <- fh.unify.safeFoldLeft(hd, b)(f) | ||
t <- ft.safeFoldLeft(tl, h)(f) | ||
} yield t | ||
} | ||
} | ||
|
||
// Induction step for coproducts | ||
implicit def mkTraverseCcons[F[_]](implicit icc: IsCCons1[F, TraverseOrMk, MkTraverse]): MkTraverse[F] = | ||
new MkTraverse[F] { | ||
override def safeTraverse[G[_] : Applicative, A, B](fa: F[A])(f: A => Eval[G[B]]): Eval[G[F[B]]] = { | ||
val gUnpacked: Eval[G[Either[icc.H[B], icc.T[B]]]] = | ||
icc.unpack(fa) match { | ||
case Left(hd) => apEval[G].map(icc.fh.unify.safeTraverse(hd)(f))(Left(_)) | ||
case Right(tl) => apEval[G].map(icc.ft.safeTraverse(tl)(f))(Right(_)) | ||
} | ||
|
||
apEval[G].map(gUnpacked)(icc.pack) | ||
} | ||
|
||
def foldRight[A, B](fa: F[A], lb: Eval[B])(f: (A, Eval[B]) => Eval[B]): Eval[B] = { | ||
import icc._ | ||
unpack(fa) match { | ||
case Left(hd) => fh.unify.foldRight(hd, lb)(f) | ||
case Right(tl) => ft.foldRight(tl, lb)(f) | ||
} | ||
} | ||
|
||
def safeFoldLeft[A, B](fa: F[A], b: B)(f: (B, A) => Eval[B]): Eval[B] = { | ||
import icc._ | ||
unpack(fa) match { | ||
case Left(hd) => fh.unify.safeFoldLeft(hd, b)(f) | ||
case Right(tl) => ft.safeFoldLeft(tl, b)(f) | ||
} | ||
} | ||
} | ||
|
||
} | ||
|
||
private[derived] trait MkTraverse1 extends MkTraverse2 { | ||
implicit def mkTraverseSplit[F[_]](implicit split: Split1[F, TraverseOrMk, TraverseOrMk]): MkTraverse[F] = | ||
new MkTraverse[F] { | ||
override def safeTraverse[G[_] : Applicative, A, B](fa: F[A])(f: A => Eval[G[B]]): Eval[G[F[B]]] = | ||
split.fo.unify.safeTraverse(split.unpack(fa))(split.fi.unify.safeTraverse(_)(f)).map(_.map(split.pack)) | ||
|
||
def foldRight[A, B](fa: F[A], lb: Eval[B])(f: (A, Eval[B]) => Eval[B]): Eval[B] = { | ||
import split._ | ||
fo.unify.foldRight(unpack(fa), lb) { (fai, lbi) => fi.unify.foldRight(fai, lbi)(f) } | ||
} | ||
|
||
def safeFoldLeft[A, B](fa: F[A], b: B)(f: (B, A) => Eval[B]): Eval[B] = { | ||
import split._ | ||
fo.unify.safeFoldLeft(unpack(fa), b) { (lbi, fai) => fi.unify.safeFoldLeft(fai, lbi)(f) } | ||
} | ||
} | ||
} | ||
|
||
private[derived] trait MkTraverse2 extends MkTraverseUtils { | ||
implicit def mkTraverseGeneric[F[_]](implicit gen: Generic1[F, MkTraverse]): MkTraverse[F] = | ||
new MkTraverse[F] { | ||
override def safeTraverse[G[_] : Applicative, A, B](fa: F[A])(f: A => Eval[G[B]]): Eval[G[F[B]]] = | ||
gen.fr.safeTraverse(gen.to(fa))(f).map(_.map(gen.from)) | ||
|
||
def foldRight[A, B](fa: F[A], lb: Eval[B])(f: (A, Eval[B]) => Eval[B]): Eval[B] = | ||
gen.fr.foldRight(gen.to(fa), lb)(f) | ||
|
||
def safeFoldLeft[A, B](fa: F[A], b: B)(f: (B, A) => Eval[B]): Eval[B] = | ||
gen.fr.safeFoldLeft(gen.to(fa), b)(f) | ||
} | ||
} | ||
|
||
private[derived] trait MkTraverseUtils { | ||
|
||
protected type TraverseOrMk[F[_]] = Traverse[F] OrElse MkTraverse[F] | ||
|
||
protected def apEval[G[_] : Applicative] = Applicative[Eval].compose[G] | ||
|
||
protected implicit class SafeTraverse[F[_]](val F: Traverse[F]) { | ||
def safeTraverse[G[_] : Applicative, A, B](fa: F[A])(f: A => Eval[G[B]]): Eval[G[F[B]]] = F match { | ||
case mk: MkTraverse[F] => mk.safeTraverse(fa)(f) | ||
case _ => F.traverse[λ[t => Eval[G[t]]], A, B](fa)(f)(apEval[G]) | ||
} | ||
|
||
def safeFoldLeft[A, B](fa: F[A], b: B)(f: (B, A) => Eval[B]): Eval[B] = F match { | ||
case mff: MkTraverse[F] => mff.safeFoldLeft(fa, b)(f) | ||
case _ => Now(F.foldLeft(fa, b) { (b, a) => f(b, a).value }) | ||
} | ||
} | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,163 @@ | ||
package cats | ||
package derived | ||
|
||
import cats.derived.TestDefns._ | ||
import cats.implicits._ | ||
import cats.laws.discipline.{FoldableTests, TraverseTests} | ||
import org.scalacheck.Test | ||
import org.scalatest.FreeSpec | ||
import shapeless.test.illTyped | ||
|
||
class TraverseSuite extends FreeSpec { | ||
|
||
"traverse" - { | ||
|
||
"passes cats traverse tests" in { | ||
Test.checkProperties( | ||
Test.Parameters.default, | ||
TraverseTests[IList](semi.traverse[IList]).traverse[Int, Double, String, Long, Option, Option].all | ||
) | ||
} | ||
|
||
"derives an instance for" - { | ||
// occasional map, as a special case of traverse[Id,_], is just fine and is easier to test | ||
// the laws were checked in the previous test | ||
|
||
"for a Tree" in { | ||
implicit val F = semi.traverse[Tree] | ||
|
||
val tree: Tree[String] = | ||
Node( | ||
Leaf("12"), | ||
Node( | ||
Leaf("3"), | ||
Leaf("4") | ||
) | ||
) | ||
|
||
val expected: List[Tree[Char]] = List( | ||
Node( | ||
Leaf('1'), | ||
Node( | ||
Leaf('3'), | ||
Leaf('4') | ||
) | ||
), | ||
Node( | ||
Leaf('2'), | ||
Node( | ||
Leaf('3'), | ||
Leaf('4') | ||
) | ||
) | ||
) | ||
|
||
assert(tree.traverse(_.toCharArray.toList) == expected) | ||
} | ||
|
||
"for a nested List[List[_]] (with alias)" in { | ||
illTyped("derive.traverse[λ[t => List[List[t]]]]") | ||
type LList[T] = List[List[T]] | ||
val F = semi.traverse[LList] | ||
|
||
val l = List(List(1), List(2, 3), List(4, 5, 6), List(), List(7)) | ||
val expected = List(List(2), List(3, 4), List(5, 6, 7), List(), List(8)) | ||
|
||
assert(F.map(l)(_ + 1) == expected) | ||
} | ||
|
||
"for a pair on the left (with alias)" in { | ||
illTyped("derive.traverse[(?, String)]") | ||
|
||
def F[R]: Traverse[(?, R)] = { | ||
type Pair[L] = (L, R) | ||
semi.traverse[Pair] | ||
} | ||
|
||
val pair = (42, "shapeless") | ||
assert(F[String].map(pair)(_ / 2) == (21, "shapeless")) | ||
} | ||
|
||
"for a pair on the right" in { | ||
def F[L]: Traverse[(L, ?)] = semi.traverse[(L, ?)] | ||
|
||
val pair = (42, "shapeless") | ||
assert(F[Int].map(pair)(_.length) == (42, 9)) | ||
} | ||
|
||
} | ||
|
||
"respects existing instances for a generic ADT " in { | ||
implicit val F = semi.traverse[GenericAdt] | ||
val adt: GenericAdt[Int] = GenericAdtCase(Some(2)) | ||
assert(adt.map(_ + 1) == GenericAdtCase(Some(3))) | ||
} | ||
|
||
"is stack safe" in { | ||
implicit val F = semi.traverse[IList] | ||
|
||
val llarge = List.range(1, 10000) | ||
val large = IList.fromSeq(llarge) | ||
|
||
val actual = large.traverse[Option, Int](i => Option(i + 1)).map(IList.toList) | ||
val expected = Option(llarge.map(_ + 1)) | ||
|
||
assert(actual == expected) | ||
} | ||
|
||
"auto derivation work for interleaved case class" in { | ||
import cats.derived.auto.traverse._ | ||
|
||
val testInstance = TestTraverse(1, "ab", 2.0, List("cd"), "3") | ||
|
||
val expected = List( | ||
TestTraverse(1, 'a', 2.0, List('c'), "3"), | ||
TestTraverse(1, 'a', 2.0, List('d'), "3"), | ||
TestTraverse(1, 'b', 2.0, List('c'), "3"), | ||
TestTraverse(1, 'b', 2.0, List('d'), "3") | ||
) | ||
val actual = testInstance.traverse(_.toCharArray.toList) | ||
|
||
assert(actual == expected) | ||
} | ||
|
||
"folds" - { | ||
|
||
"pass cats fold tests" in { | ||
Test.checkProperties( | ||
Test.Parameters.default, | ||
FoldableTests[IList](semi.traverse[IList]).foldable[Int, Double].all | ||
) | ||
} | ||
|
||
"are implemented correctly" in { | ||
implicit val F = semi.traverse[IList].asInstanceOf[MkTraverse[IList]] | ||
|
||
val iList = IList.fromSeq(List.range(1, 5)) | ||
|
||
// just basic sanity checks | ||
assert(F.foldLeft(iList, "x")(_ + _) == "x1234") | ||
assert(F.foldRight(iList, Now("x"))((i, b) => b.map(i + _)).value == "1234x") | ||
assert(F.foldMap(iList)(_.toDouble) == 10) | ||
} | ||
|
||
"are stack safe" in { | ||
implicit val F = semi.traverse[IList] | ||
|
||
val n = 10000 | ||
val llarge = IList.fromSeq(List.range(1, n)) | ||
|
||
val expected = n * (n - 1) / 2 | ||
val evalActual = F.foldRight(llarge, Now(0))((buff, eval) => eval.map(_ + buff)) | ||
|
||
assert(evalActual.value == expected) | ||
} | ||
} | ||
} | ||
|
||
implicit def eqTestClass[T: Eq]: Eq[IList[T]] = semi.eq | ||
|
||
} | ||
|
||
private case class TestTraverse[T](i: Int, t: T, d: Double, tt: List[T], s: String) | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters