-
-
Notifications
You must be signed in to change notification settings - Fork 1.2k
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
Changes from all commits
2d14873
d435297
e524108
157efa0
6bb406d
7f448b6
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
package cats.free | ||
|
||
private trait FreeStructuralInstances |
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 | ||
} | ||
} | ||
} |
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]] | ||
|
||
// TODO HashLaws#sameAsUniversalHash is really dodgy | ||
// checkAll("Free[Option, Int]", HashTests[Free[Option, Int]].hash) | ||
checkAll("Free[Option, Int]", PartialOrderTests[Free[Option, Int]].partialOrder) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. is it the case that 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? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
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 The stronger bidirectional implication definitely doesn't hold, since 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. |
||
checkAll("Free[ExprF, String]", EqTests[Free[ExprF, String]].eqv) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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] | ||
} | ||
} |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep