Skip to content

Commit

Permalink
Implement MkTraverse (#103)
Browse files Browse the repository at this point in the history
* 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
letalvoj authored and kailuowang committed Sep 8, 2018
1 parent 533a5e1 commit 2528d17
Show file tree
Hide file tree
Showing 5 changed files with 346 additions and 1 deletion.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ Instances derivations are available for the following type classes:
* `Hash`
* `Functor`
* `Foldable`
* `Traverse`
* `Show`
* `Monoid` and `MonoidK`
* `Semigroup` and `SemigroupK`
Expand Down
15 changes: 15 additions & 0 deletions core/src/main/scala/cats/derived/package.scala
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ object auto {

object foldable extends MkFoldableDerivation

object traverse extends MkTraverseDerivation

}

Expand Down Expand Up @@ -139,6 +140,18 @@ object cached {
: Functor[F] = ev.value
}

object foldable {
implicit def kittensMkFoldable[F[_]](
implicit refute: Refute[Foldable[F]], ev: Cached[MkFoldable[F]])
: Foldable[F] = ev.value
}

object traverse{
implicit def kittensMkTraverse[F[_]](
implicit refute: Refute[Traverse[F]], ev: Cached[MkTraverse[F]])
: Traverse[F] = ev.value
}

object show {
implicit def kittensMkshow[A](
implicit refute: Refute[Show[A]], ev: Cached[MkShow[A]])
Expand Down Expand Up @@ -225,6 +238,8 @@ object semi {

def foldable[F[_]](implicit F: MkFoldable[F]): Foldable[F] = F

def traverse[F[_]](implicit F: MkTraverse[F]): Traverse[F] = F

def monoid[T](implicit T: MkMonoid[T]): Monoid[T] = T

def monoidK[F[_]](implicit F: MkMonoidK[F]): MonoidK[F] = F
Expand Down
166 changes: 166 additions & 0 deletions core/src/main/scala/cats/derived/traverse.scala
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 })
}
}

}
163 changes: 163 additions & 0 deletions core/src/test/scala/cats/derived/traverse.scala
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)

Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import cats.derived._
import org.scalacheck.Prop.forAll


class TraverseSuite extends KittensSuite {
class TraverseHListSuite extends KittensSuite {

def optToValidation[T](opt: Option[T]): Validated[String, T] = Validated.fromOption(opt, "Nothing Here")

Expand Down

0 comments on commit 2528d17

Please sign in to comment.