Поскольку я определяю интерпретатор с большим количеством переменных, я пишу это:

type Context = Map[String, Int]
abstract class Expr
case class Let(varname: String, varvalue: Expr, body: Expr) extends Expr
case class Var(name: String) extends Expr
case class Plus(a: Expr, b: Expr) extends Expr
case class Num(i: Int) extends Expr

def eval(expr: Expr)(implicit ctx: Context): Int = expr match {
  case Let(i, e, b) => eval(b)(ctx + (i -> eval(e)))
  case Var(s) => ctx(s)
  case Num(i) => i
  case Plus(a, b) => eval(a) + eval(b)
}

Для очень длинных выражений это не удается из-за StackOverflowException для выражений типа:

Let("a", 1, 
Let("b", Plus("a", "a"), 
Let("c", Plus("b", "a"), 
Let("d", 1,  ...  )

Однако, как только значение переменной определено, мне просто нужно снова вызвать вычислитель в теле Let, мне кажется, что он должен просто выполнить какую-то частичную хвостовую рекурсию.
Как можно добиться частичной хвостовой рекурсии в Scala?

0
Mikaël Mayer 25 Ноя 2016 в 20:44

2 ответа

Лучший ответ

Вам нужен какой-то способ получить оптимизацию хвостового вызова только для некоторых ветвей eval. Я не думаю, что это возможно - самое большее, что Scala сделает, это примет @tailrec к методу в целом и завершится ошибкой во время компиляции, если он не может оптимизировать метод в цикл.

Однако сделать эту итерацию, чтобы воспользоваться хвостовым вызовом с Let, довольно просто:

def eval(expr: Expr, ctx: Context): Int = {

  // The expression/context pair we try to reduce at every loop iteration
  var exprM = expr;
  var ctxM = ctx;

  while (true) {
    expr match {
      case Var(s) => return ctxM(s)
      case Num(i) => return i
      case Plus(a, b) => return eval(a,ctxM) + eval(b,ctxM)
      case Let(i, e, b) => {
        ctxM += i -> eval(e,ctxM). // Update ctxM
        exprM = b                  // Update exprM
      }
    }
  }
  return 0; // unreachable, but Scala complains otherwise I'm not returning 'Int'
} 

Обратите внимание, что это не решит проблемы переполнения стека из-за длинных цепочек Plus s - мы мало что можем с ними сделать, потому что рекурсивные вызовы не находятся в хвостовой позиции.

Было время, когда я думал, что Scala сделает какую-нибудь аннотацию @tailcall, чтобы иметь дело с подобными вещами, но я не уверен, что сейчас есть такой большой интерес к таким вещам.

1
Alec 25 Ноя 2016 в 18:50

Вам следует избегать использования return в scala. В этом сценарии вы можете использовать флаг для элемента управления while. например

var result = Option.empty[Int]
while (result.isEmpty) {
  ...
  result = ctxM(s)
  ...
}
result

Есть и другие (лучше IMO) способы сделать это. Например, https://typelevel.org/cats/datatypes/freemonad.html.

0
MS-H 18 Дек 2019 в 20:20