-
-
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
remove casts from Eval, fix stack overflow in Eval #3519
Changes from 3 commits
9a81bb7
fbc4dab
fef839f
8ce139f
12eebe6
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 |
---|---|---|
|
@@ -263,26 +263,13 @@ object Eval extends EvalInstances { | |
* of FlatMap nodes) or "value" (in the case of Now, Later, or | ||
* Always nodes). | ||
*/ | ||
@tailrec private def advance[A](fa: Eval[A]): Eval[A] = | ||
fa match { | ||
@tailrec private def advance[A](fa: Defer[A]): Eval[A] = | ||
fa.thunk() match { | ||
case call: Eval.Defer[A] => | ||
advance(call.thunk()) | ||
case compute: Eval.FlatMap[A] => | ||
new Eval.FlatMap[A] { | ||
type Start = compute.Start | ||
val start: () => Eval[Start] = () => compute.start() | ||
val run: Start => Eval[A] = s => advance1(compute.run(s)) | ||
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. @non can you see why we did this? Can you write a test that shows the need? Removing it is a bit more efficient since we don't add a level of stack to all FlatMaps we call, BUT note, we should only really need to call advance when we know it is a Defer (note I change the argument type). |
||
} | ||
advance(call) | ||
case other => other | ||
} | ||
|
||
/** | ||
* Alias for advance that can be called in a non-tail position | ||
* from an otherwise tailrec-optimized advance. | ||
*/ | ||
private def advance1[A](fa: Eval[A]): Eval[A] = | ||
advance(fa) | ||
|
||
/** | ||
* FlatMap is a type of Eval[A] that is used to chain computations | ||
* involving .map and .flatMap. Along with Eval#flatMap it | ||
|
@@ -318,52 +305,70 @@ object Eval extends EvalInstances { | |
} | ||
} | ||
|
||
private def evaluate[A](e: Eval[A]): A = { | ||
type L = Eval[Any] | ||
type M = Memoize[Any] | ||
type C = Any => Eval[Any] | ||
/* | ||
* This represents the stack of flatmap functions in a series | ||
* of Eval operations | ||
*/ | ||
sealed abstract private class FnStack[A, B] | ||
final private case class Ident[A, B](ev: A <:< B) extends FnStack[A, B] | ||
final private case class Many[A, B, C](first: A => Eval[B], rest: FnStack[B, C]) extends FnStack[A, C] | ||
|
||
def addToMemo(m: M): C = { (a: Any) => | ||
private def evaluate[A](e: Eval[A]): A = { | ||
def addToMemo[A1](m: Memoize[A1]): A1 => Eval[A1] = { (a: A1) => | ||
m.result = Some(a) | ||
Now(a) | ||
} | ||
|
||
@tailrec def loop(curr: L, fs: List[C]): Any = | ||
@tailrec def loop[A1](curr: Eval[A1], fs: FnStack[A1, A]): A = | ||
curr match { | ||
case c: FlatMap[_] => | ||
case c: FlatMap[A1] => | ||
c.start() match { | ||
case cc: FlatMap[_] => | ||
loop(cc.start().asInstanceOf[L], cc.run.asInstanceOf[C] :: c.run.asInstanceOf[C] :: fs) | ||
case cc: FlatMap[c.Start] => | ||
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 not entirely sure why we don't do: case c: FlatMap[A1] =>
loop(c.start(), Many(c.run, fs)) and let the rest of the code run. That would be correct, but I assume @non inlined this because for a FlatMap we know that we have at least one item on the stack and if we do my suggestion above, we allocate a If @non has time to chime in, it would be appreciated, I could update a comment to make it clearer. |
||
val nextFs = Many(c.run, fs) | ||
loop(cc.start(), Many(cc.run, nextFs)) | ||
case call: Defer[c.Start] => | ||
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 branch needs to be added. Note I added a test to the PR the exercises this. Without this change we blow the stack. |
||
// though the flatMap method handles defer(x).flatMap(f) | ||
// by removing the Defer, we can nest defers, | ||
// so defer(defer(x)).flatMap(f) could mean c.start() | ||
// returns a Defer. We have to handle it here | ||
loop(advance(call), Many(c.run, fs)) | ||
case mm @ Memoize(eval) => | ||
mm.result match { | ||
case Some(a) => | ||
loop(Now(a), c.run.asInstanceOf[C] :: fs) | ||
loop(c.run(a), fs) | ||
case None => | ||
loop(eval, addToMemo(mm.asInstanceOf[M]) :: c.run.asInstanceOf[C] :: fs) | ||
val nextFs = Many(c.run, fs) | ||
loop(eval, Many(addToMemo(mm), nextFs)) | ||
} | ||
case xx => | ||
// xx must be Now, Later, Always, all of those | ||
// have safe .value: | ||
loop(c.run(xx.value), fs) | ||
} | ||
case call: Defer[_] => | ||
case call: Defer[A1] => | ||
loop(advance(call), fs) | ||
case m @ Memoize(eval) => | ||
case m: Memoize[a] => | ||
// a <:< A1 | ||
m.result match { | ||
case Some(a) => | ||
fs match { | ||
case f :: fs => loop(f(a), fs) | ||
case Nil => a | ||
case Many(f, fs) => loop(f(a), fs) | ||
case Ident(ev) => ev(a) | ||
} | ||
case None => | ||
loop(eval, addToMemo(m.asInstanceOf[M]) :: fs) | ||
loop[a](m.eval, Many[a, A1, A](addToMemo[a](m), fs)) | ||
} | ||
case x => | ||
// Now, Later or Always don't have recursions | ||
// so they have safe .value: | ||
val a1 = x.value | ||
fs match { | ||
case f :: fs => loop(f(x.value), fs) | ||
case Nil => x.value | ||
case Many(f, fs) => loop(f(a1), fs) | ||
case Ident(ev) => ev(a1) | ||
} | ||
} | ||
|
||
loop(e.asInstanceOf[L], Nil).asInstanceOf[A] | ||
loop(e, Ident(implicitly[A <:< 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. here we have 1 allocation more than we did before: |
||
} | ||
} | ||
|
||
|
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.
this is a private method, and we only need to call it on
Defer[A]
to return a non-Defer result. This simplifies the code since I don't see any reason we need to deal with FlatMap as we did.