Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added implementation of Eq[Free] #3949

Merged
merged 6 commits into from
Aug 9, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package cats.free

private trait FreeStructuralInstances
129 changes: 129 additions & 0 deletions free/src/main/scala-2.13+/cats/free/FreeStructuralInstances.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
package cats
package free

/**
* Free may be viewed either as a sort of sequential program construction mechanism, wherein
* programs are assembled and then finally interpreted via foldMap, or it may be viewed as a
* recursive data structure shaped by the ''pattern'' of its suspension functor. In this view,
* it is helpful to think of Free as being the following recursive type alias:
*
* {{{
* type Free[S[_], A] = Either[S[Free[S, A]], A]
* }}}
*
* Thus, a Free is a tree of Either(s) in which the data (of type A) is at the leaves, and each
* branching point is defined by the shape of the S functor. This kind of structural interpretation
* of Free is very useful in certain contexts, such as when representing expression trees in
* compilers and interpreters.
*
* Using this interpretation, we can define many common instances over the ''data structure'' of
* Free, most notably Eq and Traverse (Show is also possible, though far less useful). This makes
* it much more convenient to use Free structures as if they were conventional data structures.
*
* Unfortunately, this functionality fundamentally requires recursive implicit resolution. This
* feature was added to Scala in 2.13 (and retained in Scala 3) in the form of by-name implicits,
* but is fundamentally unavailable in Scala 2.12 and earlier without using something like Shapeless.
* For that reason, all of these instances are only available on 2.13 and above.
*/
private trait FreeStructuralInstances extends FreeStructuralInstances0

private trait FreeStructuralInstances0 extends FreeStructuralInstances1 {

implicit def catsFreeShowForFree[S[_], A](implicit
SF: Functor[S],
S: => Show[S[Free[S, A]]],
A: Show[A]
): Show[Free[S, A]] =
Show.show { fsa =>
fsa.resume match {
case Right(a) => A.show(a)
case Left(sfa) => S.show(sfa)
}
}

implicit def catsFreeHashForFree[S[_], A](implicit
SF: Functor[S],
S0: => Hash[S[Free[S, A]]],
A0: Hash[A]
): Hash[Free[S, A]] =
new FreeStructuralHash[S, A] {
def functor = SF
def S = S0
def A = A0
}

trait FreeStructuralHash[S[_], A] extends FreeStructuralEq[S, A] with Hash[Free[S, A]] {
implicit override def S: Hash[S[Free[S, A]]]
implicit override def A: Hash[A]

def hash(fsa: Free[S, A]): Int =
fsa.resume match {
case Right(a) => A.hash(a)
case Left(sfa) => S.hash(sfa)
}
}
}

private trait FreeStructuralInstances1 extends FreeStructuralInstances2 {

implicit def catsFreePartialOrderForFree[S[_], A](implicit
SF: Functor[S],
S0: => PartialOrder[S[Free[S, A]]],
A0: PartialOrder[A]
): PartialOrder[Free[S, A]] =
new FreeStructuralPartialOrder[S, A] {
def functor = SF
def S = S0
def A = A0
}

trait FreeStructuralPartialOrder[S[_], A] extends PartialOrder[Free[S, A]] {
implicit def functor: Functor[S]
implicit def S: PartialOrder[S[Free[S, A]]]
implicit def A: PartialOrder[A]

def partialCompare(left: Free[S, A], right: Free[S, A]): Double =
(left.resume, right.resume) match {
case (Right(leftA), Right(rightA)) =>
A.partialCompare(leftA, rightA)

case (Left(leftS), Left(rightS)) =>
S.partialCompare(leftS, rightS)

case (Left(_), Right(_)) | (Right(_), Left(_)) =>
Double.NaN
}
}
}

private trait FreeStructuralInstances2 {

implicit def catsFreeEqForFree[S[_], A](implicit
SF: Functor[S],
S0: => Eq[S[Free[S, A]]],
A0: Eq[A]
): Eq[Free[S, A]] =
new FreeStructuralEq[S, A] {
def functor = SF
def S = S0
def A = A0
}

trait FreeStructuralEq[S[_], A] extends Eq[Free[S, A]] {
implicit def functor: Functor[S]
implicit def S: Eq[S[Free[S, A]]]
implicit def A: Eq[A]

def eqv(left: Free[S, A], right: Free[S, A]): Boolean =
(left.resume, right.resume) match {
case (Right(leftA), Right(rightA)) =>
A.eqv(leftA, rightA)

case (Left(leftS), Left(rightS)) =>
S.eqv(leftS, rightS)

case (Left(_), Right(_)) | (Right(_), Left(_)) =>
false
}
}
}
2 changes: 1 addition & 1 deletion free/src/main/scala/cats/free/Free.scala
Original file line number Diff line number Diff line change
Expand Up @@ -344,7 +344,7 @@ private trait FreeTraverse[F[_]] extends Traverse[Free[F, *]] with FreeFoldable[
final override def map[A, B](fa: Free[F, A])(f: A => B): Free[F, B] = fa.map(f)
}

sealed abstract private[free] class FreeInstances extends FreeInstances1 {
sealed abstract private[free] class FreeInstances extends FreeInstances1 with FreeStructuralInstances {

/**
* `Free[S, *]` has a monad for any type constructor `S[_]`.
Expand Down
133 changes: 133 additions & 0 deletions free/src/test/scala-2.13+/cats/free/FreeStructuralSuite.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
package cats.free

import cats.{Applicative, Eq, Eval, Functor, Show, Traverse}
import cats.kernel.laws.discipline.{EqTests, /*HashTests,*/ PartialOrderTests}
import cats.syntax.all._
import cats.tests.CatsSuite

import org.scalacheck.{Arbitrary, Cogen, Gen}

// this functionality doesn't exist on Scala 2.12
class FreeStructuralSuite extends CatsSuite {
import FreeSuite.freeArbitrary
import FreeStructuralSuite._

implicit def freeCogen[S[_]: Functor, A](implicit S: => Cogen[S[Free[S, A]]], A: Cogen[A]): Cogen[Free[S, A]] =
Cogen { (seed, f) =>
f.resume match {
case Left(sf) =>
S.perturb(seed, sf)

case Right(a) =>
A.perturb(seed, a)
}
}

Show[Free[Option, Int]]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this just for checking that you can summon the show instance?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep


// TODO HashLaws#sameAsUniversalHash is really dodgy
// checkAll("Free[Option, Int]", HashTests[Free[Option, Int]].hash)
checkAll("Free[Option, Int]", PartialOrderTests[Free[Option, Int]].partialOrder)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is it the case that f1: Free[Option, Int] == f2: Free[Option, Int] <=> f1.runTailRec == f2.runTailRec?

That seems like a nice property to have, but I don't see that if the current code can get it.

If it can't get it, what does this notion of equality mean?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess if I read the docs I might learn something...

So, I guess the property I mention is not true, but one way: if we are "free-equal" then we are option-equal.

That might be a nice test to add to strengthen the tests here (e.g. I think returning that all items are equivalent always passes the laws, but isn't what we really want, adding a law I suggest here rules out many bad implementations since happening to get equal Option[Int] is unlikely.

Copy link
Member Author

@djspiewak djspiewak Aug 9, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So, I guess the property I mention is not true, but one way: if we are "free-equal" then we are option-equal.

I believe this is the case. Caffeine is still working its way into my brain, but at first glance, I think this property holds at least because Either is monotone with respect to Eq (meaning that a1 === a2 implies Right(a1) === Right(a2) and Left(a1) === Left(a2) and the converse), which means that this would reduce to an inductive argument over that monotonicity. But, as I'll argue in a minute, this restriction on the pattern functor is probably not needed.

The stronger bidirectional implication definitely doesn't hold, since Free.pure(42).runTailRec === Free.suspend(Some(42)).runTailRec, but they would not be considered equal by the structural definition of Eq. You can build versions of this which don't rely on coyoneda as well. Interpreting a Free into a monad is a stronger process than simply inspecting its defining structure, which is what this Eq does. In a sense, this is the reification of the intuition that, for almost any value, there are many different programs which can produce that outcome. The structural Eq inspects the program, while runTailRec inspects the outcome.

What you're saying is that, for any programs that are structurally equal, the outcomes they produce when run must also be equal. I believe this holds for all suspension functors which are deterministic monads (e.g. race makes IO a nondeterministic monad, which is why it is generally ignored as a combinator). Of course, this property is only defined for suspension functors which also form monads, which will not be true in general for most of the functors with which this kind of technique is useful, so I'm not sure we need to formalize this intuition.

checkAll("Free[ExprF, String]", EqTests[Free[ExprF, String]].eqv)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

}

object FreeStructuralSuite {
type Expr[A] = Free[ExprF, A]

// a pattern functor for a simple expression language
sealed trait ExprF[A] extends Product with Serializable

object ExprF {

implicit def eq[A: Eq]: Eq[ExprF[A]] =
Eq.instance {
case (Add(left1, right1), Add(left2, right2)) =>
left1 === left2 && right1 === right2

case (Neg(inner1), Neg(inner2)) =>
inner1 === inner2

case (Num(value1), Num(value2)) =>
value1 === value2

case (_, _) =>
false
}

implicit def traverse: Traverse[ExprF] =
new Traverse[ExprF] {

def foldLeft[A, B](fa: ExprF[A], b: B)(f: (B, A) => B): B =
fa match {
case Add(left, right) =>
f(f(b, left), right)

case Neg(inner) =>
f(b, inner)

case Num(_) =>
b
}

def foldRight[A, B](fa: ExprF[A], lb: Eval[B])(f: (A, Eval[B]) => Eval[B]): Eval[B] =
fa match {
case Add(left, right) =>
f(left, f(right, lb))

case Neg(inner) =>
f(inner, lb)

case Num(_) =>
lb
}

def traverse[G[_]: Applicative, A, B](fa: ExprF[A])(f: A => G[B]): G[ExprF[B]] =
fa match {
case Add(left, right) =>
(f(left), f(right)).mapN(Add(_, _))

case Neg(inner) =>
f(inner).map(Neg(_))

case Num(value) =>
Applicative[G].pure(Num(value))
}
}

implicit def arbitraryExprF[A: Arbitrary](implicit gnum: Arbitrary[Int]): Arbitrary[ExprF[A]] =
Arbitrary {
import Arbitrary.arbitrary

val genAdd: Gen[Add[A]] =
for {
left <- arbitrary[A]
right <- arbitrary[A]
} yield Add(left, right)

val genNeg: Gen[Neg[A]] =
arbitrary[A].map(Neg(_))

val genNum: Gen[Num[A]] = gnum.arbitrary.map(Num(_))

Gen.oneOf(genAdd, genNeg, genNum)
}

implicit def cogenExprF[A](implicit cg: Cogen[A], cgnum: Cogen[Int]): Cogen[ExprF[A]] =
Cogen { (seed, ef) =>
ef match {
case Add(left, right) =>
cg.perturb(cg.perturb(seed, left), right)

case Neg(inner) =>
cg.perturb(seed, inner)

case Num(value) =>
cgnum.perturb(seed, value)
}
}

final case class Add[A](left: A, right: A) extends ExprF[A]
final case class Neg[A](inner: A) extends ExprF[A]
final case class Num[A](value: Int) extends ExprF[A]
}
}