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

Add a Continuation data type #1400

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from 2 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
108 changes: 108 additions & 0 deletions core/src/main/scala/cats/data/Continuation.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
package cats
package data

import java.io.Serializable
import scala.annotation.tailrec

sealed abstract class Continuation[O, +I] extends Serializable {
Copy link
Contributor

@adelbertc adelbertc Oct 4, 2016

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 the Monad instance

Copy link
Contributor

@erikerlandson erikerlandson Oct 4, 2016

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 variances

Copy link
Contributor

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 in I as opposed to R. with Continuation[I, O] you would have Monad[Continuation[?, O]] as opposed to Monad[Continuation[O, ?]].

Copy link
Contributor

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.

Copy link
Contributor Author

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 the apply 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.

Copy link
Contributor Author

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?

Copy link
Contributor

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 in I => O, it seemed as though it would make sense to have it be -I, analogous to Function[-I, +O], however that doesn't take into account the ways it is different.

Copy link
Contributor

@non non Oct 7, 2016

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 an apply method as follows:

def apply(fn: I => O): O

In the context of Function1[I, O] the I is contravariant (as you point out). However this function itself is used in a contravariant position (an input parameter) to the apply method. This means that in the context of Continutation 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 its map method (e.g. def map[A, B](f: A => B): List[B]).

EDIT: Turns out @johnynek said the same thing below. Jinx!

def map[I2](fn: I => I2): Continuation[O, I2] =
Continuation.Mapped(this, fn)

def flatMap[I2](fn: I => Continuation[O, I2]): Continuation[O, I2] =
Continuation.FlatMapped(this, fn)

final def apply(fn: I => O): O = {
import Continuation._
val re = new RunEnv[O]
re.eval(this, re.ScalaFn(fn)).get
}
}

object Continuation {
def pure[O] = new PureBuilder[O]
class PureBuilder[O] {
Copy link
Contributor

@adelbertc adelbertc Oct 4, 2016

Choose a reason for hiding this comment

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

final and private[Continuation] ? The former for sure, the latter mostly because I saw that's how we've been doing it: https://github.com/typelevel/cats/blob/master/core/src/main/scala/cats/data/OptionT.scala#L167

def apply[I](i: I): Continuation[O, I] = Const(i)
}
def from[I, O](fn: (I => O) => O): Continuation[O, I] = Cont(fn)
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]

Copy link
Contributor

@adelbertc adelbertc Oct 4, 2016

Choose a reason for hiding this comment

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

Don't know if also marking these as final does anything but maybe for style consistency?

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 {
Copy link
Contributor

Choose a reason for hiding this comment

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

extends Product with Serializable, final case classes?

Copy link
Contributor

Choose a reason for hiding this comment

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

the parent class is sealed, so case classes are already final, aren't they?

Copy link
Contributor

Choose a reason for hiding this comment

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

Yeah but it still bothers me from a style perspective ;)

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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
Copy link

Choose a reason for hiding this comment

The 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]
Copy link
Contributor

Choose a reason for hiding this comment

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

extends Product with Serializable, final case classes

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]
}
}
50 changes: 50 additions & 0 deletions tests/src/test/scala/cats/tests/ContinuationTests.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package cats
package tests

import cats.data.Continuation
import cats.laws.discipline._
import org.scalacheck.{ Arbitrary, Gen }

class ContinuationTests extends CatsSuite {

implicit def arbContFn[O, I](implicit I: Arbitrary[I], O: Arbitrary[O]): Arbitrary[(I => O) => O] = {
def call(i: I): (I => O) => O = { fn => fn(i) }
def const(o: O): (I => O) => O = { fn => o }

Arbitrary(Gen.oneOf(I.arbitrary.map(call(_)), O.arbitrary.map { o => const(o) }))
}
implicit def arbCont[O, I](implicit I: Arbitrary[I], fn: Arbitrary[(I => O) => O]): Arbitrary[Continuation[O, I]] =
Copy link
Contributor

Choose a reason for hiding this comment

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

Should this go in cats.laws.discipline.arbitrary

Arbitrary(Gen.oneOf(I.arbitrary.map(Continuation.pure[O](_)),
fn.arbitrary.map(Continuation.from(_))))

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)
}
}