Tail call optimization in Ruby with trampolines

Posted on April 17, 2015

The idea is taken from Clojure.

def trampoline
  f = yield
  loop do
    case f
    when Proc
      f = f[]
    else
      break f
    end
  end
end

def fact(n, acc)
  if n > 0
    -> { fact(n - 1, acc * n) }
  else
    acc
  end
end

trampoline { fact(100000, 1) }
#=> 282422940796034787429342157802453551847749492609...

def fib(a, b, n)
  if n > 0
    -> { fib(b, a + b, n - 1)}
  else
    a
  end
end

(0..9).map { |i| trampoline { fib(0, 1, i) } }
#=> [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]

trampoline { fib(0, 1, 100000) }
#=> 259740693472217241661550340212759...

# Mutual recursion

def isEven(xs)
  xs.empty? || -> { isOdd(xs[1..-1]) }
end

def isOdd(xs)
  !xs.empty? && -> { isEven(xs[1..-1]) }
end

trampoline { isEven((0..10000).to_a) } #=> false
trampoline { isOdd((0..10000).to_a)  } #=> true
trampoline { isEven((0..10001).to_a) } #=> true
trampoline { isOdd((0..10001).to_a)  } #=> false

There is also a more strict approach taken from Scala, but it seems a bit heavy for Ruby:

class Done
  attr_reader :a

  def initialize(a)
    @a = a
  end

  def result
    a
  end
end

def Done(a)
  Done.new(a)
end

class Call
  attr_reader :rest

  def initialize
    @rest = -> { yield }
  end

  def result
    next_r = rest[]
    loop do
      case next_r
      when Done
        break next_r.a
      when Call
        next_r = next_r.rest[]
      end
    end
  end
end

def Call
  Call.new {
    yield
  }
end

def isEven(xs)
  xs.empty? ? Done(true) : Call { isOdd(xs[1..-1]) }
end

def isOdd(xs)
  xs.empty? ? Done(false) : Call { isEven(xs[1..-1]) }
end

isEven((0..9999).to_a).result   #=> true
isOdd((0..9999).to_a).result    #=> false
isEven((0..10000).to_a).result  #=> false
isOdd((0..10000).to_a).result   #=> true

def fact(n, acc)
  if n > 0
    Call { fact(n - 1, acc * n) }
  else
    Done(acc)
  end
end

fact(100000, 1).result
#=> 28242294079603478742934215780245355184...