Open xysun opened 6 years ago
Here's a working example for lazy noobs like me:
import scala.math.BigInt
// import $ivy.`org.typelevel::cats-core:1.1.0`
import cats._, cats.data._, cats.implicits._
def factorial(n : BigInt) : Eval[BigInt] =
if (n == 1) Eval.now(1)
else Eval.defer(factorial(n-1).map(_ * n))
factorial(10000).value
Hi Pavel! π
Hi Joy! ππ
It's worth noting as the article you referenced mentions, trampolining unfortunately isn't as cost-free as the while
loops that scalac
can compile from plain tail recursion: it trades stack frames for heap space.
That obviously goes a long way and is easy to accept for many situations, but there has been a lot of buzz around the pure FP Scala world lately about the performance costs of the "rub some Free Monads on all the things!" craze where entire large programs and all their data flow may be done this way⦠:neckbeard:
By the way, I've just been going through the Scala with Cats book that Underscore made free, and it's pretty good and approachable.
π
Hi Ches! π Well guess which book I'm reading that triggered this post :)
π± π πΈ
This note is about my understanding for the
Eval
type in cats, specifically, the fact that itsMap
andflatMap
are so called "stack free", which is driven by a data structure calledTrampoline
.Start with classical factorial example:
Code above is not stack safe. To calculate
factorial(5)
, at one moment there will be 5 functions on the stack, each waiting for the next to complete.You can easily blow up stack by calling
factorial(10000)
-- stack will have to hold 10000 functions at the same time.We know we can easily fix this by turning it into a tail recursive function:
With this function, at any time, no matter how big the initial input is, there is only one function on stack.
when you call
factorial(5, 1)
, this function is created and placed on stack first. However, it quickly gets replaced byfactorial(4, 5)
, then it gets replaced again byfactorial(3, 20)
, and so on.Many recursive functions can be re-written in a tail recursive manner, but it's tedious!
Eval.map
orEval.flatMap
is an abstraction for writing tail recursive, stack safe functions.Use
Eval.Now
as base case, then replace any non tail-recursive operation by calling.map
, then wrap inEval.Defer
To get the actual result, call
.value
on the returnedEval[Int]
, in cats it's implemented by aevaluate[A](v:Eval[A])
function. The real trick is this function is implemented tail recursively (so eachevaluate()
call gets replaced by a newevaluate()
call), so you get tail recursion for free, without thinking how to turn it to tail recursion.This is not something special about
Eval
though. Behind the curtain,Eval
implements an internal data structure calledTrampoline
, which is defined as below:Another nice thing is
Eval
is by itself a monad, meaning you can use it in for comprehensions:Now this for comprehension is also stack safe: it will only keep one function on the stack, instead of all the
f
s.References