dgkf / R

An experimental reimagining of R

Home Page:https://dgkf.github.io/R

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

How should tail call recursion be handled?

dgkf opened this issue · comments

Inspired by recent advances in the R language, I went ahead and implemented a form of tail call recursion. However, it necessarily executes in a "non-standard" way, greedily evaluating arguments to recursive calls.

This means that expectations of R would apply to most calls, but might be unexpected for tail-recursive calls.

Typical Evaluation

f <- function(n, result) {
  if (n > 0) { 
    cat(n, "\n")
    f(n - 1, result)
  } else {
    result
  }
}

f(3, stop("Done"))
#> 3
#> 2
#> 1
#> Error: Done

Tail-Recursive Evaluation

f <- function(n, result) {
 if (n > 0) { 
   cat(n, "\n")
   f(n - 1, result)
 } else {
   result
 }
}

f(3, stop("Done"))
#> Error: Done

This is because result is necessarily greedily evaluated. Recursive calls will not work with promises dependent on the parent frame or non-standard evaluation. The "right" approach here is a bit unclear, so I'm going to wait to see how the R version feels with all of R's non-standard eval tricks to see if I can learn something from the design.

Was mulling over this today and I think there happy middle ground where tail call optimization only happens if all arguments are evaluated (equivalent to forceed in R).

no_tail_call_opt <- function(n, msg) {
  if (n > 0) {
    no_tail_call_opt(n - 1, msg)
  } else {
    msg
  }
}

tail_call_opt <- function(n, msg) {
  if (n > 0) {
    force(msg)
    tail_call_opt(n - 1, msg)
  } else {
    msg
  }
}

When a recursive call overflows the call stack, an error message could even suggest the change.