Skip to content

Commit

Permalink
Rework Foldable derivation
Browse files Browse the repository at this point in the history
* Make it more consistent with other derivations
* Add more tests
* Make sure it's stack safe
* Make sure `forall` and `exists` are lazy
  • Loading branch information
joroKr21 committed Apr 27, 2019
1 parent 58cc026 commit 8223a1c
Show file tree
Hide file tree
Showing 3 changed files with 166 additions and 132 deletions.
143 changes: 67 additions & 76 deletions core/src/main/scala/cats/derived/foldable.scala
Original file line number Diff line number Diff line change
Expand Up @@ -16,114 +16,105 @@

package cats.derived

import cats.{ Eval, Foldable }, Eval.now
import cats.{Eval, Foldable}
import shapeless._

import scala.annotation.implicitNotFound

@implicitNotFound("Could not derive an instance of Foldable[${F}]")
trait MkFoldable[F[_]] extends Foldable[F] {
def foldLeft[A, B](fa: F[A], b: B)(f: (B, A) => B): B = safeFoldLeft(fa, b){ (b, a) => now(f(b, a)) }.value
def foldRight[A, B](fa: F[A], lb: Eval[B])(f: (A, Eval[B]) => Eval[B]): Eval[B]
def safeFoldLeft[A, B](fa: F[A], b: B)(f: (B, A) => Eval[B]): Eval[B]

def foldLeft[A, B](fa: F[A], b: B)(f: (B, A) => B): B =
safeFoldLeft(fa, b)((b, a) => Eval.later(f(b, a))).value

// The default `forall` is not lazy.
override def forall[A](fa: F[A])(p: A => Boolean): Boolean =
foldRight(fa, Eval.True)((a, lb) => lb.map(_ && p(a))).value

// The default `exists` is not lazy.
override def exists[A](fa: F[A])(p: A => Boolean): Boolean =
foldRight(fa, Eval.False)((a, lb) => lb.map(_ || p(a))).value
}

object MkFoldable extends MkFoldableDerivation {
def apply[F[_]](implicit mff: MkFoldable[F]): MkFoldable[F] = mff
def apply[F[_]](implicit F: MkFoldable[F]): MkFoldable[F] = F
}

trait MkFoldableDerivation extends MkFoldable0 {
implicit val mkFoldableId: MkFoldable[shapeless.Id] =
new MkFoldable[shapeless.Id] {
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)
private[derived] abstract class MkFoldableDerivation extends MkFoldableNested {
implicit val mkFoldableHNil: MkFoldable[Const[HNil]#λ] = mkFoldableConst
implicit val mkFoldableCNil: MkFoldable[Const[CNil]#λ] = mkFoldableConst

implicit def mkFoldableConst[T]: MkFoldable[Const[T]#λ] =
new MkFoldable[Const[T]#λ] {
def foldRight[A, B](fa: T, lb: Eval[B])(f: (A, Eval[B]) => Eval[B]) = lb
def safeFoldLeft[A, B](fa: T, b: B)(f: (B, A) => Eval[B]) = Eval.now(b)
}
}

override implicit def mkFoldableConstFoldable[T]: MkFoldable[Const[T]#λ] =
super[MkFoldable0].mkFoldableConstFoldable
private[derived] abstract class MkFoldableNested extends MkFoldableCons {

implicit def mkFoldableNested[F[_]](implicit F: Split1[F, FoldableOrMk, FoldableOrMk]): MkFoldable[F] =
new MkFoldable[F] {

def foldRight[A, B](fa: F[A], lb: Eval[B])(f: (A, Eval[B]) => Eval[B]) =
F.fo.unify.foldRight(F.unpack(fa), lb)(F.fi.unify.foldRight(_, _)(f))

def safeFoldLeft[A, B](fa: F[A], b: B)(f: (B, A) => Eval[B]) =
mkSafeFoldLeft(F.fo)(F.unpack(fa), b)((b, fia) => mkSafeFoldLeft(F.fi)(fia, b)(f))
}
}

trait MkFoldable0 extends MkFoldable1 {
// Induction step for products
implicit def mkFoldableHcons[F[_]](implicit ihc: IsHCons1[F, Foldable, MkFoldable]): MkFoldable[F] =
private[derived] abstract class MkFoldableCons extends MkFoldableGeneric {

implicit def mkFoldableHCons[F[_]](implicit F: IsHCons1[F, FoldableOrMk, MkFoldable]): MkFoldable[F] =
new MkFoldable[F] {
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.foldRight(hd, now(t))(f)
} yield h

def foldRight[A, B](fa: F[A], lb: Eval[B])(f: (A, Eval[B]) => Eval[B]) = {
val (fha, fta) = F.unpack(fa)
F.ft.foldRight(fta, lb)(f).flatMap(t => F.fh.unify.foldRight(fha, Eval.now(t))(f))
}

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.safeFoldLeft(hd, b)(f)
t <- ft.safeFoldLeft(tl, h)(f)
} yield t
def safeFoldLeft[A, B](fa: F[A], b: B)(f: (B, A) => Eval[B]) = {
val (fha, fta) = F.unpack(fa)
mkSafeFoldLeft(F.fh)(fha, b)(f).flatMap(F.ft.safeFoldLeft(fta, _)(f))
}
}

// Induction step for coproducts
implicit def mkFoldableCcons[F[_]](implicit icc: IsCCons1[F, Foldable, MkFoldable]): MkFoldable[F] =
implicit def mkFoldableCCons[F[_]](implicit F: IsCCons1[F, FoldableOrMk, MkFoldable]): MkFoldable[F] =
new MkFoldable[F] {
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.foldRight(hd, lb)(f)
case Right(tl) => ft.foldRight(tl, lb)(f)

def foldRight[A, B](fa: F[A], lb: Eval[B])(f: (A, Eval[B]) => Eval[B]) =
F.unpack(fa) match {
case Left(fha) => F.fh.unify.foldRight(fha, lb)(f)
case Right(fta) => F.ft.foldRight(fta, 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.safeFoldLeft(hd, b)(f)
case Right(tl) => ft.safeFoldLeft(tl, b)(f)
def safeFoldLeft[A, B](fa: F[A], b: B)(f: (B, A) => Eval[B]) =
F.unpack(fa) match {
case Left(fha) => mkSafeFoldLeft(F.fh)(fha, b)(f)
case Right(fta) => F.ft.safeFoldLeft(fta, b)(f)
}
}
}
}

trait MkFoldable1 extends MkFoldable2 {
implicit def mkFoldableSplit[F[_]](implicit split: Split1[F, Foldable, Foldable]): MkFoldable[F] =
new MkFoldable[F] {
def foldRight[A, B](fa: F[A], lb: Eval[B])(f: (A, Eval[B]) => Eval[B]): Eval[B] = {
import split._
fo.foldRight(unpack(fa), lb) { (fai, lbi) => fi.foldRight(fai, lbi)(f) }
}
private[derived] abstract class MkFoldableGeneric {
protected type FoldableOrMk[F[_]] = Foldable[F] OrElse MkFoldable[F]

def safeFoldLeft[A, B](fa: F[A], b: B)(f: (B, A) => Eval[B]): Eval[B] = {
import split._
fo.safeFoldLeft(unpack(fa), b){ (lbi, fai) => fi.safeFoldLeft(fai, lbi)(f) }
}
protected def mkSafeFoldLeft[F[_], A, B](F: FoldableOrMk[F])(fa: F[A], b: B)(f: (B, A) => Eval[B]): Eval[B] =
F.unify match {
case mk: MkFoldable[F] => mk.safeFoldLeft(fa, b)(f)
case other => Eval.later(other.foldLeft(fa, b)(f(_, _).value))
}
}

trait MkFoldable2 extends MkFoldable3 {
implicit def mkFoldableGeneric[F[_]](implicit gen: Generic1[F, MkFoldable]): MkFoldable[F] =
implicit def mkFoldableGeneric[F[_]](implicit F: Generic1[F, MkFoldable]): MkFoldable[F] =
new MkFoldable[F] {
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)
}
}

trait MkFoldable3 {
def foldRight[A, B](fa: F[A], lb: Eval[B])(f: (A, Eval[B]) => Eval[B]) =
F.fr.foldRight(F.to(fa), lb)(f)

// For binary compatibility.
def mkFoldableConstFoldable[T]: MkFoldable[Const[T]#λ] =
new MkFoldable[Const[T]#λ] {
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)
def safeFoldLeft[A, B](fa: F[A], b: B)(f: (B, A) => Eval[B]) =
F.fr.safeFoldLeft(F.to(fa), b)(f)
}

implicit class FoldableSafeFoldLeft[F[_]](val ff: Foldable[F]) {
def safeFoldLeft[A, B](fa: F[A], b: B)(f: (B, A) => Eval[B]): Eval[B] =
ff match {
case mff: MkFoldable[F] => mff.safeFoldLeft(fa, b)(f)
case _ => now(ff.foldLeft(fa, b) { (b, a) => f(b, a).value })
}
}
}
13 changes: 9 additions & 4 deletions core/src/main/scala/cats/derived/package.scala
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,12 @@ object auto {
): MonoidK[F] = ev
}

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

object traverse {
implicit def kittensMkTraverse[F[_]](
implicit refute: Refute[Traverse[F]], F: Lazy[MkTraverse[F]]
Expand All @@ -105,7 +111,6 @@ object auto {

//todo: the regular approach doesn't work for the following instances
object pure extends MkPureDerivation
object foldable extends MkFoldableDerivation

object consK {
implicit def kittensMkConsK[F[_]](
Expand Down Expand Up @@ -159,8 +164,8 @@ object cached {

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

object traverse{
Expand Down Expand Up @@ -264,7 +269,7 @@ object semi {

def showPretty[A](implicit ev: MkShowPretty[A]): ShowPretty[A] = ev

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

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

Expand Down
142 changes: 90 additions & 52 deletions core/src/test/scala/cats/derived/foldable.scala
Original file line number Diff line number Diff line change
Expand Up @@ -14,69 +14,107 @@
* limitations under the License.
*/

package cats.derived
package cats
package derived

import cats.{ Eq, Eval, Foldable }, Eval.now
import cats.instances.all._
import cats.laws.discipline.FoldableTests
import org.scalacheck.Arbitrary

import TestDefns._
class FoldableSuite extends KittensSuite {
// disable scalatest ===
override def convertToEqualizer[T](left: T): Equalizer[T] = ???
import FoldableSuite._
import TestDefns._

// exists method written in terms of foldRight
def contains[F[_]: Foldable, A: Eq](as: F[A], goal: A): Eval[Boolean] =
as.foldRight(now(false)) { (a, lb) =>
if (a === goal) now(true) else lb
type OptList[A] = Option[List[A]]
type ListSnoc[A] = List[Snoc[A]]
type AndChar[A] = (A, Char)
type BoxNel[A] = Box[Nel[A]]

def testFoldable(context: String)(
implicit iList: Foldable[IList],
tree: Foldable[Tree],
genericAdt: Foldable[GenericAdt],
optList: Foldable[OptList],
listSnoc: Foldable[ListSnoc],
andChar: Foldable[AndChar],
interleaved: Foldable[Interleaved],
boxNel: Foldable[BoxNel]
): Unit = {
checkAll(s"$context.Foldable[IList]", FoldableTests[IList].foldable[Int, Long])
checkAll(s"$context.Foldable[Tree]", FoldableTests[Tree].foldable[Int, Long])
checkAll(s"$context.Foldable[GenericAdt]", FoldableTests[GenericAdt].foldable[Int, Long])
checkAll(s"$context.Foldable[OptList]", FoldableTests[OptList].foldable[Int, Long])
checkAll(s"$context.Foldable[ListSnoc]", FoldableTests[ListSnoc].foldable[Int, Long])
checkAll(s"$context.Foldable[AndChar]", FoldableTests[AndChar].foldable[Int, Long])
checkAll(s"$context.Foldable[Interleaved]", FoldableTests[Interleaved].foldable[Int, Long])
checkAll(s"$context.Foldable[BoxNel]]", FoldableTests[BoxNel].foldable[Int, Long])

val n = 10000
val large = IList.fromSeq(1 until n)

test(s"$context.Foldable.foldLeft is stack safe") {
val actual = large.foldLeft(0)(_ + _)
val expected = n * (n - 1) / 2
assert(actual == expected)
}

test(s"$context.Foldable.foldRight is stack safe") {
val actual = large.foldRight(Eval.Zero)((i, sum) => sum.map(_ + i))
val expected = n * (n - 1) / 2
assert(actual.value == expected)
}

test(s"$context.Foldable respects existing instances") {
val tail = List.range(1, 100)
val sum = boxNel.fold(Box(Nel(42, tail)))
assert(sum == tail.sum)
}
}

import auto.foldable._
import cats.instances.int._

test("Foldable[IList]") {
val F = Foldable[IList]

// some basic sanity checks
val lns = (1 to 10).toList
val ns = IList.fromSeq(lns)
val total = lns.sum
assert(F.foldLeft(ns, 0)(_ + _) == total)
assert(F.foldRight(ns, now(0))((x, ly) => ly.map(x + _)).value == total)
assert(F.fold(ns) == total)

// more basic checks
val lnames = List("Aaron", "Betty", "Calvin", "Deirdra")
val names = IList.fromSeq(lnames)
assert(F.foldMap(names)(_.length) == lnames.map(_.length).sum)

// test trampolining
val llarge = 1 to 10000
val large = IList.fromSeq(llarge)
val largeTotal = llarge.sum
assert(F.foldLeft(large, 0)(_ + _) == largeTotal)
assert(F.fold(large) == largeTotal)
assert(contains(large, 10000).value)

// safely build large lists
val larger = F.foldRight(large, now(List.empty[Int]))((x, lxs) => lxs.map((x + 1) :: _))
assert(larger.value == llarge.map(_ + 1))
{
import auto.foldable._
testFoldable("auto")
}

test("derives an instance for Interleaved[T]") {
semi.foldable[TestDefns.Interleaved]
{
import cached.foldable._
testFoldable("cached")
}

test("foldable.semi[Tree]") {
val F = semi.foldable[Tree]
semiTests.run()

val tree: Tree[String] =
Node(
Leaf("quux"),
Node(
Leaf("foo"),
Leaf("wibble")
)
)
object semiTests {
implicit val iList: Foldable[IList] = semi.foldable
implicit val tree: Foldable[Tree] = semi.foldable
implicit val genericAdt: Foldable[GenericAdt] = semi.foldable
implicit val optList: Foldable[OptList] = semi.foldable
implicit val listSnoc: Foldable[ListSnoc] = semi.foldable
implicit val andChar: Foldable[AndChar] = semi.foldable
implicit val interleaved: Foldable[Interleaved] = semi.foldable
implicit val boxNel: Foldable[BoxNel] = semi.foldable
def run(): Unit = testFoldable("semi")
}
}

object FoldableSuite {

final case class Nel[+A](head: A, tail: List[A])
object Nel {

assert(F.foldLeft(tree, 0)(_ + _.length) == 13)
implicit def eqv[A](implicit A: Eq[A]): Eq[Nel[A]] = {
val listEq = Eq[List[A]]
Eq.instance((x, y) => A.eqv(x.head, y.head) && listEq.eqv(x.tail, y.tail))
}

implicit def arbitrary[A: Arbitrary]: Arbitrary[Nel[A]] =
Arbitrary(for {
head <- Arbitrary.arbitrary[A]
tail <- Arbitrary.arbitrary[List[A]]
} yield Nel(head, tail))

implicit val foldable: Foldable[Nel] = new Foldable[Nel] {
def foldLeft[A, B](fa: Nel[A], b: B)(f: (B, A) => B) = fa.tail.foldLeft(b)(f)
def foldRight[A, B](fa: Nel[A], lb: Eval[B])(f: (A, Eval[B]) => Eval[B]) = fa.tail.foldRight(lb)(f)
}
}
}

0 comments on commit 8223a1c

Please sign in to comment.