Tail Call Optimization in JavaScript

in #tco6 years ago

[JavaScript] 尾调用优化 Tail Call Optimization

介绍

现在函数式编程越来越流行,有时我们会选择用recursion递归来写逻辑,这样代码容易懂而且也可以避免一些side-effects副作用,但是, 但是,JavaScript并不支持尾递归调用

举个栗子🌰,下面👇代码会出错

var sum = function(x, y) {
  if (y > 0) {
    return sum(x + 1, y - 1)
  } else {
    return x
  }
}

sum(1, 100000) // RangeError: Maximum call stack size exceeded

var factorial = function (n) {
  function recur (n, acc) {
    if (n === 0) {
      return acc
    } else {
      return recur(n-1, n*acc)
    }
  }
  return recur(n, 1)
}

factorial(100000) // RangeError: Maximum call stack size exceeded

stack

从上图可以看出来,每次调用都会依赖上一次调用的结果,所以会一直新建stack直到溢出,因为调用的最后一步不是直接返回调用结果而是计算再返回

解决方案一

我们可以定义一个函数trampoline,它接受一个函数func作为参数,每次调用func都会返回一个新的函数,我们循环调用直到返回的结果不是函数

但是这个方案的不足在于,入侵性比较强,你需要更改原来的函数,让它返回一个binded函数,bind会耗时又耗内存,因为每次都bind出来一个新的函数

var trampoline = function (func, arg) {
  var value = func(arg)
  while (typeof value === 'function') {
    value = value()
  }
  return value
}

function factorial (n) {
  function recur (n, acc) {
    if (n === 0) {
      return acc
    } else {
      return recur.bind(null, n-1, n*acc)
    }
  }
  return function () {
    return recur.bind(null, n, 1)
  }
}

console.log( trampoline(factorial(100000)) )
// => Infinity

解决方案二

这个解决方案是利用数组来保存每次调用的参数,第一次调用时,active是false,所以我们保存参数到数组中,并且进入到while循环,再次调用时active是true,所以每次都只是保存参数到数组中并返回,直到函数停止调用自身,参数没有保存到数组,从而返回最后的结果给value

该方案入侵性小,不需要改变原函数,但是性能方面,相比非递归的循环,慢了不少

var tco = function (f) {
  var value
  var active = false
  var accumulated = []

  return function accumulator () {
    accumulated.push(arguments)
    if (!active) {
      active = true
      while (accumulated.length) {
        value = f.apply(this, accumulated.shift())
      }
      active = false
      return value
    }
  }
}

const sum = tco(function(x, y) {
  if (y > 0) {
    return sum(x + 1, y - 1)
  } else {
    return x
  }
})

console.log( sum(1, 100000) ) // => 100001

结论

在JavaScript完全支持尾递归调用之前,上面的方案可以临时使用

发布了一个lib tco-node方便大家使用,星星✨是对我最大的鼓励