-
-
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
Add Defer typeclass, laws and implementations #2279
Changes from 6 commits
a9aed70
36f4252
6f9339a
2b07112
fd43163
c1ebba7
f3df8ef
3e25d2c
c86ece0
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,33 @@ | ||
package cats | ||
|
||
/** | ||
* Defer is a type class that shows the ability to defer creation | ||
* inside of the type constructor F[_]. | ||
* | ||
* This comes up with F[_] types that are implemented with a trampoline | ||
* or are based on function application. | ||
* | ||
* The law is that defer(fa) is equivalent to fa, but not evaluated immediately, | ||
* so | ||
* {{{ | ||
* import cats._ | ||
* import cats.implicits._ | ||
* | ||
* var evaluated = false | ||
* val dfa = | ||
* Defer[Eval].defer { | ||
* evaluated = true | ||
* Eval.now(21) | ||
* } | ||
* | ||
* assert(!evaluated) | ||
* Eq[Eval[Int]].eqv(dfa, Eval.now(21)) | ||
* }}} | ||
*/ | ||
trait Defer[F[_]] extends Serializable { | ||
def defer[A](fa: => F[A]): F[A] | ||
} | ||
|
||
object Defer { | ||
def apply[F[_]](implicit defer: Defer[F]): Defer[F] = defer | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
package cats | ||
package laws | ||
|
||
import catalysts.Platform | ||
/** | ||
* Laws that must be obeyed by any `Defer`. | ||
*/ | ||
trait DeferLaws[F[_]] { | ||
implicit def F: Defer[F] | ||
|
||
def deferIdentity[A](fa: Unit => F[A]): IsEq[F[A]] = | ||
F.defer(fa(())) <-> fa(()) | ||
|
||
def deferDoesNotEvaluate[A](fa: Unit => F[A]): IsEq[Boolean] = { | ||
var evaluated = false | ||
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. Unsuspended vars in laws make me feel uneasy. First of all I fear that such laws will not have a long life. To fix it, you can defer the whole thing: def deferDoesNotEvaluate[A](f: Boolean => F[A]): IsEq[Boolean] = {
val lh = F.defer {
var evaluated = false
F.defer {
evaluated = true
f(evaluated)
}
f(evaluated)
}
lh <-> f(false)
} 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’m nervous that if you pass a function that ignores the Boolean this is true trivially. |
||
val deferUnit = F.defer { | ||
evaluated = true; | ||
fa(()) | ||
} | ||
evaluated <-> false | ||
} | ||
|
||
def deferIsStackSafe[A](fa: Unit => F[A]): IsEq[F[A]] = { | ||
def loop(c: Int): F[A] = | ||
if (c <= 0) F.defer(fa(())) | ||
else F.defer(loop(c - 1)) | ||
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. This law is interesting, but in case an Also the question on my mind would be: is Note that we also have a 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. That's a good question about making the law stronger: if there is a monad, flatMap must be stack safe. The function cases wouldn't pass this law. As to what might not be a monad: command line parsing is often done with an applicative, not a monad (see optparse-applicative library in haskell or https://github.com/bkirwi/decline for a cats version). Those types can still have a lawful Defer. 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. If we have |
||
|
||
val cnt = if (Platform.isJvm) 20000 else 2000 | ||
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. In my experience these arbitrary values hurt for In Cats-Effect what I did was to introduce an And in absence of that, I'd use smaller values: val cnt = if (Platform.isJvm) 10000 else 1000 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 don’t see a compelling reason why this is only 2x too large. Can you give an idea? |
||
loop(cnt) <-> (fa(())) | ||
} | ||
} | ||
|
||
object DeferLaws { | ||
def apply[F[_]](implicit ev: Defer[F]): DeferLaws[F] = | ||
new DeferLaws[F] { def F: Defer[F] = ev } | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
package cats | ||
package laws | ||
package discipline | ||
|
||
import org.scalacheck.{Arbitrary, Prop} | ||
import Prop._ | ||
import org.typelevel.discipline.Laws | ||
|
||
trait DeferTests[F[_]] extends Laws { | ||
def laws: DeferLaws[F] | ||
|
||
def defer[A: Arbitrary](implicit | ||
ArbFA: Arbitrary[F[A]], | ||
EqFA: Eq[F[A]], | ||
EqBool: Eq[Boolean] | ||
): RuleSet = { | ||
new DefaultRuleSet( | ||
name = "defer", | ||
parent = None, | ||
"defer Identity" -> forAll(laws.deferIdentity[A] _), | ||
"defer does not evaluate" -> forAll(laws.deferDoesNotEvaluate[A] _), | ||
"defer is stack safe" -> forAll(laws.deferIsStackSafe[A] _)) | ||
} | ||
} | ||
|
||
object DeferTests { | ||
def apply[F[_]: Defer]: DeferTests[F] = | ||
new DeferTests[F] { def laws: DeferLaws[F] = DeferLaws[F] } | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -30,13 +30,35 @@ class FunctionSuite extends CatsSuite { | |
import Helpers._ | ||
|
||
checkAll("Function0[Int]", SemigroupalTests[Function0].semigroupal[Int, Int, Int]) | ||
// TODO: make an binary compatible way to do this | ||
implicit val deferFunction0: Defer[Function0] = | ||
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. One way to do this that I can think of is to just write this in a new |
||
new Defer[Function0] { | ||
case class Deferred[A](fa: () => Function0[A]) extends Function0[A] { | ||
def apply() = { | ||
@annotation.tailrec | ||
def loop(f: () => Function0[A]): A = | ||
f() match { | ||
case Deferred(f) => loop(f) | ||
case next => next() | ||
} | ||
loop(fa) | ||
} | ||
} | ||
def defer[A](fa: => Function0[A]): Function0[A] = { | ||
lazy val cachedFa = fa | ||
Deferred(() => cachedFa) | ||
} | ||
} | ||
checkAll("Function0[Int]", DeferTests[Function0].defer[Int]) | ||
checkAll("Semigroupal[Function0]", SerializableTests.serializable(Semigroupal[Function0])) | ||
|
||
checkAll("Function0[Int]", BimonadTests[Function0].bimonad[Int, Int, Int]) | ||
checkAll("Bimonad[Function0]", SerializableTests.serializable(Bimonad[Function0])) | ||
|
||
implicit val iso = SemigroupalTests.Isomorphisms.invariant[Function1[Int, ?]] | ||
checkAll("Function1[Int, Int]", SemigroupalTests[Function1[Int, ?]].semigroupal[Int, Int, Int]) | ||
// TODO: make an binary compatible way to do this | ||
//checkAll("Function1[Int => ?]", DeferTests[Function1[Int, ?]].defer[Int]) | ||
checkAll("Semigroupal[Function1[Int, ?]]", SerializableTests.serializable(Semigroupal[Function1[Int, ?]])) | ||
|
||
checkAll("Function1[Int, Int]", MonadTests[Int => ?].monad[Int, Int, Int]) | ||
|
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.
missing
T
aftercatsDataDeferForIor
?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.
looks like we are still missing the T.
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.
I got you. https://github.com/typelevel/cats/pull/2688/files