Reputation: 13
I'd like to change the code to be tail recursive not to overflow the stack expression is an ADT of Label or Tree
def combine[A](expression: Expression, runners: List[Runner[A]]): Runner[A] = {
val labelToHashmap = runners.map(runner=> (runner.label.get, runner)).toMap
def reduceExpression(e: Expression): Runner[A] = {
e match {
case Label(_, value) => labelToHashmap(value)
case Tree(_, operation, left, right) =>
operation match {
case AND => reduceExpression(left).and(reduceExpression(right))
case OR => reduceExpression(left).or(reduceExpression(right))
}
}
}
reduceExpression(expression)
}
How can I turn the above code to a tail recursive one?
Upvotes: 1
Views: 152
Reputation: 4577
You can rewrite the function in a tail-recursive way like Kolmar has shown, and it'll work. But I think this usually obscures the intent of the algorithm, because now you have to fiddle around with an explicit stack that is normally implicit.
I would say it's better to factor out the stack fiddling bits into a re-usable data structure and use that. The cats.Eval
type is such a data structure.
import cats.Eval
import cats.syntax.all._
def combine[A](
expression: Expression,
runners: List[Runner[A]]
): Runner[A] = {
val labelToHashmap = runners.fproductLeft(_.label.get).toMap
def reduceExpression(e: Expression): Eval[Runner[A]] =
Eval.now(e).flatMap {
case Label(_, value) => labelToHashmap(value)
case Tree(_, operation, left, right) =>
operation match {
case AND =>
(
reduceExpression(left),
reduceExpression(right)
).mapN(_ and _)
case OR =>
(
reduceExpression(left),
reduceExpression(right)
).mapN(_ or _)
}
}
reduceExpression(expression).value
}
As you can see, this essentially retains the logic of the straight-up recursive implementation, but it's nevertheless stack-safe due to the way Eval
's value
method is implemented.
Also check out the documentation: https://typelevel.org/cats/datatypes/eval.html
Upvotes: 2
Reputation: 14224
As @JörgWMittag has commented, to process a tree with tail recursion you have to transform the computation, and the most straightforward way is to simulate the call stack and pass it to the recursive calls:
def combine[A](expression: Expression, runners: List[Runner[A]]): Runner[A] = {
val labelToRunner = runners.map(runner => (runner.label.get, runner)).toMap
sealed trait Element
case class Op(operation: Operation) extends Element
case class Expr(expression: Expression) extends Element
case class Result(runner: Runner[A]) extends Element
@tailrec
def reduce(stack: List[Element]): Runner[A] = {
def expand(expression: Expression): List[Element] = expression match {
case Label(_, value) =>
List(Result(labelToRunner(value)))
case Tree(_, operation, left, right)=>
List(Expr(left), Expr(right), Op(operation))
}
stack match {
case List(Result(runner)) => runner
case Expr(expression) :: rest =>
reduce(expand(expression) ::: rest)
case (left @ Result(_)) :: Expr(expression) :: rest =>
// The subtree we are processing is put on the top of the stack
// Thus when the operation is applied the elements are in reverse order
reduce(expand(expression) ::: left :: rest)
case Result(right) :: Result(left) :: Op(operation) :: rest =>
val combined = operation match {
case AND => left.and(right)
case OR => left.or(right)
}
reduce(Result(combined) :: rest)
}
}
reduce(List(Expr(expression)))
}
It's interesting to print out the trace of that function to see how it first expands expressions, transforms simple expressions into results, and applies operations. For example here is the stack for some expression on mock runners with numberic labels:
Expr(((1 AND (2 OR 3)) OR 4) AND ((5 OR 6) AND 7))
Expr((1 AND (2 OR 3)) OR 4), Expr((5 OR 6) AND 7), Op(AND)
Expr(1 AND (2 OR 3)), Expr(4), Op(OR), Expr((5 OR 6) AND 7), Op(AND)
Expr(1), Expr(2 OR 3), Op(AND), Expr(4), Op(OR), Expr((5 OR 6) AND 7), Op(AND)
Result(1), Expr(2 OR 3), Op(AND), Expr(4), Op(OR), Expr((5 OR 6) AND 7), Op(AND)
Expr(2), Expr(3), Op(OR), Result(1), Op(AND), Expr(4), Op(OR), Expr((5 OR 6) AND 7), Op(AND)
Result(2), Expr(3), Op(OR), Result(1), Op(AND), Expr(4), Op(OR), Expr((5 OR 6) AND 7), Op(AND)
Result(3), Result(2), Op(OR), Result(1), Op(AND), Expr(4), Op(OR), Expr((5 OR 6) AND 7), Op(AND)
Result(2 OR 3), Result(1), Op(AND), Expr(4), Op(OR), Expr((5 OR 6) AND 7), Op(AND)
Result(1 AND (2 OR 3)), Expr(4), Op(OR), Expr((5 OR 6) AND 7), Op(AND)
Result(4), Result(1 AND (2 OR 3)), Op(OR), Expr((5 OR 6) AND 7), Op(AND)
Result((1 AND (2 OR 3)) OR 4), Expr((5 OR 6) AND 7), Op(AND)
Expr(5 OR 6), Expr(7), Op(AND), Result((1 AND (2 OR 3)) OR 4), Op(AND)
Expr(5), Expr(6), Op(OR), Expr(7), Op(AND), Result((1 AND (2 OR 3)) OR 4), Op(AND)
Result(5), Expr(6), Op(OR), Expr(7), Op(AND), Result((1 AND (2 OR 3)) OR 4), Op(AND)
Result(6), Result(5), Op(OR), Expr(7), Op(AND), Result((1 AND (2 OR 3)) OR 4), Op(AND)
Result(5 OR 6), Expr(7), Op(AND), Result((1 AND (2 OR 3)) OR 4), Op(AND)
Result(7), Result(5 OR 6), Op(AND), Result((1 AND (2 OR 3)) OR 4), Op(AND)
Result((5 OR 6) AND 7), Result((1 AND (2 OR 3)) OR 4), Op(AND)
Result(((1 AND (2 OR 3)) OR 4) AND ((5 OR 6) AND 7))
Upvotes: 1