-
-
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 a Continuation data type #1400
Changes from all commits
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,149 @@ | ||
package cats | ||
package data | ||
|
||
import java.io.Serializable | ||
import scala.annotation.tailrec | ||
|
||
/** | ||
* A Continuation[O, +I] is isomorphic to (I => O) => O | ||
* but equiped with a monad. The monad consumes stack size | ||
* proportional to the number of `Continuation.from` instances | ||
* are embedded inside (not the number of flatMaps). | ||
* Continuation.pure and flatMap are stack safe. | ||
*/ | ||
sealed abstract class Continuation[O, +I] extends Serializable { | ||
final def map[I2](fn: I => I2): Continuation[O, I2] = | ||
Continuation.Mapped(this, fn) | ||
|
||
final def flatMap[I2](fn: I => Continuation[O, I2]): Continuation[O, I2] = | ||
Continuation.FlatMapped(this, fn) | ||
|
||
final def apply(fn: I => O): O = { | ||
val re = new Continuation.RunEnv[O] | ||
re.eval(this, re.ScalaFn(fn)).get | ||
} | ||
|
||
final def widen[I2 >: I]: Continuation[O, I2] = this | ||
} | ||
|
||
object Continuation { | ||
def pure[O]: PureBuilder[O] = new PureBuilder[O] | ||
final class PureBuilder[O] private[Continuation] { | ||
@inline | ||
def apply[I](i: I): Continuation[O, I] = Const(i) | ||
} | ||
|
||
/** | ||
* Evaluate the argument each time we call apply on | ||
* the continuation | ||
*/ | ||
def always[I, O](i: => I): Continuation[O, I] = | ||
Const(()).flatMap(_ => Const(i)) | ||
|
||
/** | ||
* Evaluate the argument on the first call to apply | ||
*/ | ||
def later[I, O](i: => I): Continuation[O, I] = { | ||
lazy val evaluated = i | ||
Const(()).flatMap(_ => Const(evaluated)) | ||
} | ||
/** | ||
* Always return a given value in the continuation | ||
*/ | ||
def fixed[O](o: O): Continuation[O, Nothing] = | ||
from(_ => o) | ||
/** | ||
* This does not evaluate Eval until apply is called | ||
* on the Continuation | ||
*/ | ||
def fromEval[I, O](e: Eval[I]): Continuation[O, I] = | ||
Const(()).flatMap(_ => Const(e.value)) | ||
/** | ||
* Wrap a function as a Continuation | ||
*/ | ||
def from[I, O](fn: (I => O) => O): Continuation[O, I] = Cont(fn) | ||
/** | ||
* Convenience to wrap scala.Function2 | ||
*/ | ||
def from2[I1, I2, O](call: (Function2[I1, I2, O]) => O): Continuation[O, (I1, I2)] = from { fn: (((I1, I2)) => O) => | ||
call { (i1: I1, i2: I2) => fn((i1, i2)) } | ||
} | ||
|
||
implicit def catsDataContinuationMonad[O]: Monad[Continuation[O, ?]] = new Monad[Continuation[O, ?]] { | ||
def pure[I](i: I): Continuation[O, I] = Const(i) | ||
override def map[I1, I2](f: Continuation[O, I1])(fn: I1 => I2): Continuation[O, I2] = f.map(fn) | ||
def flatMap[I1, I2](f: Continuation[O, I1])(fn1: I1 => Continuation[O, I2]): Continuation[O, I2] = f.flatMap(fn1) | ||
/** | ||
* flatMap is trampolined, BUT the stack depth grows proportional to the number of `from` calls | ||
*/ | ||
def tailRecM[A, B](a: A)(fn: A => Continuation[O, Either[A, B]]): Continuation[O, B] = | ||
fn(a).flatMap { | ||
case Right(b) => Const(b) | ||
case Left(a) => tailRecM(a)(fn) | ||
} | ||
} | ||
|
||
private case class Const[O, I](i: I) extends Continuation[O, I] | ||
|
||
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. Don't know if also marking these as |
||
private case class Mapped[O, I1, I2]( | ||
c: Continuation[O, I1], | ||
fn: I1 => I2) extends Continuation[O, I2] | ||
|
||
private case class FlatMapped[O, I1, I2]( | ||
c: Continuation[O, I1], | ||
fn: I1 => Continuation[O, I2]) extends Continuation[O, I2] | ||
|
||
private case class Cont[O, I](runfn: (I => O) => O) extends Continuation[O, I] | ||
|
||
/** | ||
* We hide all our implementation in this class. Note there is only | ||
* ever one output, so we put the type here. | ||
*/ | ||
private class RunEnv[O] { | ||
sealed abstract class Result { | ||
def get: O | ||
} | ||
case class Complete(get: O) extends Result | ||
case class ContResult[I](runfn: (I => O) => O, fn: Fn[I]) extends Result { | ||
def get: O = fn match { | ||
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.
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. the parent class is 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. Yeah but it still bothers me from a style perspective ;) 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. do we really need to do needless work just for style? There are a lot of classes here all hidden inside a private class. |
||
case ScalaFn(sfn) => runfn(sfn) | ||
case _ => | ||
// This is not stack safe, we could keep finding continuations | ||
// the stack depth will scale as the number of inner continuations, | ||
// not flatMaps (which you can see | ||
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 comment didn't quite get finished. |
||
runfn { i: I => eval(Const(i), fn).get } | ||
} | ||
} | ||
|
||
/** | ||
* This evaluates a continuation given an Fn | ||
*/ | ||
final def eval[I](c: Continuation[O, I], fn: Fn[I]): Result = | ||
loop(c.asInstanceOf[Continuation[O, Any]], fn.asInstanceOf[Fn[Any]]) | ||
|
||
/** | ||
* This is the tail recursive loop that partially evaluates until | ||
* we reach the next continuation. Note, it compiles with the type below: | ||
* | ||
* final def loop[I](c: Continuation[O, I], fn: Fn[I]): Result = c match { | ||
* | ||
* but we erase I to Any so scala tailrec optimization can work | ||
*/ | ||
@tailrec | ||
private def loop(c: Continuation[O, Any], fn: Fn[Any]): Result = c match { | ||
case Cont(k) => ContResult(k, fn) | ||
case Mapped(c, mfn) => loop(c, AndThen(mfn, fn)) | ||
case FlatMapped(c, next) => loop(c, RunCont(next, fn)) | ||
case Const(i) => fn match { | ||
case ScalaFn(sfn) => Complete(sfn(i)) | ||
case RunCont(mkC, next0) => loop(mkC(i), next0) | ||
case AndThen(sfn, next) => loop(Const(sfn(i)), next) | ||
} | ||
} | ||
|
||
sealed abstract class Fn[-A] | ||
case class ScalaFn[A](fn: A => O) extends Fn[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.
|
||
case class RunCont[A, B](fn: A => Continuation[O, B], next: Fn[B]) extends Fn[A] | ||
case class AndThen[A, B](fn: A => B, next: Fn[B]) extends Fn[A] | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,70 @@ | ||
package cats | ||
package tests | ||
|
||
import cats.data.Continuation | ||
import cats.laws.discipline._ | ||
import cats.laws.discipline.arbitrary._ | ||
|
||
class ContinuationTests extends CatsSuite { | ||
def eq0[I, O: Eq](fn: I => O) = new Eq[Continuation[O, I]] { | ||
def eqv(a: Continuation[O, I], b: Continuation[O, I]) = | ||
Eq[O].eqv(a(fn), b(fn)) | ||
} | ||
implicit val eqInt = eq0[Int, Int](x => x*x*x) | ||
implicit val eqInt3 = eq0[(Int, Int, Int), Int] { case (x, y, z) => x*y*z } | ||
implicit val iso = CartesianTests.Isomorphisms.invariant[Continuation[Int, ?]] | ||
|
||
checkAll("Continuation[Int, ?]", MonadTests[Continuation[Int, ?]].monad[Int, Int, Int]) | ||
checkAll("Monad[Continuation[Int, ?]]", SerializableTests.serializable(Monad[Continuation[Int, ?]])) | ||
|
||
test("continuations are called") { | ||
val c = Continuation.from2[Int, Int, Int]((0 to 100).reduce(_)) | ||
assert(c { case (a, b) => a + b } == (0 to 100).reduce(_ + _)) | ||
} | ||
test("flatMaps are sane") { | ||
val exists = Continuation.from[Int, Boolean](List(1, 2, 3, 4).exists(_)) | ||
val forall = Continuation.from[Int, Boolean](List(1, 2, 3, 4).forall(_)) | ||
val c = for { | ||
e <- exists | ||
f <- forall | ||
} yield (e, f) | ||
|
||
// Does there exist a number that is <= all the numbers? yes. | ||
assert(c { case (x, y) => (x <= y) } == true) | ||
// Does there exist a number that is >= all the numbers? yes. | ||
assert(c { case (x, y) => (x >= y) } == true) | ||
// Does there exist a number that is > all the numbers? no. | ||
assert(c { case (x, y) => (x == y) } == false) | ||
} | ||
test("Continuation.always works") { | ||
var cnt = 0 | ||
val c = Continuation.always[Int, Int] { cnt += 1; 0 } | ||
assert(cnt == 0) | ||
c(identity) | ||
assert(cnt == 1) | ||
c(identity) | ||
assert(cnt == 2) | ||
} | ||
test("Continuation.later works") { | ||
var cnt = 0 | ||
val c = Continuation.later[Int, Int] { cnt += 1; 0 } | ||
assert(cnt == 0) | ||
c(identity) | ||
assert(cnt == 1) | ||
c(identity) | ||
assert(cnt == 1) | ||
} | ||
test("Continuation.fromEval works") { | ||
var cnt = 0 | ||
val c = Continuation.fromEval[Int, Int](Eval.later { cnt += 1; 0 }) | ||
assert(cnt == 0) | ||
c(identity) | ||
assert(cnt == 1) | ||
c(identity) | ||
assert(cnt == 1) | ||
} | ||
test("Continuation.fixed works") { | ||
val c = Continuation.fixed(100).widen[String] // compiler sees Nothing and warns on calling it, since any call is dead code | ||
assert(c(_ => sys.error("never called")) == 100) | ||
} | ||
} |
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.
extends Product with Serializable
Also Scaladoc saying
Continuation[O, I]
is(I => O) => O
. Seeing the type parameters order reversed confused me for a second before I realized it was probably for theMonad
instanceThere 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 was also going to suggest a signature of
Continuation[-I, +O]
, unless there's some reason for the current order of types, and their variancesThere 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.
Assuming my algebra is right I think
O
could also be made covariant like you said. A separate but related question is if we want this to be variant at all since that comes with all the gotchas associated with variance and subtyping in Scala. I would vote no.I'm guessing the order is because it makes partial application look a bit cleaner since the
Monad
instance is free inI
as opposed toR
. withContinuation[I, O]
you would haveMonad[Continuation[?, O]]
as opposed toMonad[Continuation[O, ?]]
.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.
Good point, variances can turn into land-mines. Far easier to add them later if there is a reason, than try to remove them after the fact.
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.
no,
O
is invariant. Note that it appears in the output, so it is either invariant or covariant. But it also appears in the input of the function, which makes it invariant or contravariant (see theapply
method), so it is invariant.A more direct way to see it: if you have a function
(I => O) => O
and say,O = String
. Now due to covariance, I want to treat it as(I => Any) => Any
can I do this? It is not clear at all how since internal to a function(I => String) => String
we could make use of the String structure, but when I am given a function to Any, I can't do that.So in fact,
Continuation[O, +I]
is the correct variance.Now, about ordering of the parameters, I agree it is not the most intuitive order, but due to the the 2712 fix, right to left is the standard order searched when looking for single parameter type classes, so I think we are better off keeping the parameter there.
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.
about adding co-variance, I'd really like to keep it. Contravariance causes the most problems, I think, but covariance is very useful and common especially in container code.
Can you point to the problem that covariance causes?
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'm unaware of any particular problems relating to covariance. Since
I
plays the role of input type inI => O
, it seemed as though it would make sense to have it be-I
, analogous toFunction[-I, +O]
, however that doesn't take into account the ways it is different.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.
When you use a contravariant type in a contravariant position the variance is flipped.
So
Continuation
has anapply
method as follows:In the context of
Function1[I, O]
theI
is contravariant (as you point out). However this function itself is used in a contravariant position (an input parameter) to theapply
method. This means that in the context ofContinutation
I
is covariant (a contravariant type used in contravariant position is treated as covariant).Does that make sense to you? It might help to think about the relationship between
List[A]
and itsmap
method (e.g.def map[A, B](f: A => B): List[B]
).EDIT: Turns out @johnynek said the same thing below. Jinx!