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