【问题标题】:Scala partial tail recursionScala部分尾递归
【发布时间】:2017-04-10 03:53:38
【问题描述】:

因为我定义了一个有很多变量的解释器,所以我写这个:

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 中如何实现部分尾递归?

【问题讨论】:

  • eval(a)+eval(b) 这样的表达式不会阻止尾递归吗?必须维护堆栈,以便在eval(a) 完成后评估eval(b)?只有当返回值是对递归函数的单次调用时,尾递归才可能。
  • 显然。这就是为什么我不能进行尾递归。但是,看,在我只有很多 Lets 的情况下,我不想保留堆栈,我想像尾递归一样重用函数。一种部分尾递归。对可以尾递归的部分进行尾递归,并为其他部分增加堆栈大小。
  • 你有什么理由要避免蹦床吗?
  • 你说的tarmpolining是什么意思?

标签: scala recursion stack-overflow tail-recursion


【解决方案1】:

你应该避免在 scala 中使用return。在这种情况下,您可以为 while 控件使用标志。 例如

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

还有其他(IMO 更好的)方法可以解决这个问题。例如https://typelevel.org/cats/datatypes/freemonad.html

【讨论】:

    【解决方案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'
    } 
    

    请注意,由于Pluss 的长链,这不会解决堆栈溢出问题 - 因为递归调用不在尾部位置,我们确实无能为力。

    有一段时间我认为 Scala 会做一些 @tailcall 注释来处理这类事情,但我不确定人们是否对这类事情有那么大的兴趣。

    【讨论】:

    • 看起来您的解决方案是最佳选择。请注意,如果您想避免多次分配 exprM,您可以只写 exprM = expr match { .... case Let(i, e, b) => ctxM += i -> eval(e, ctxM); b }。在任何情况下,return 语句都会使分配短路。
    猜你喜欢
    • 2018-03-17
    • 2018-03-08
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多