How to write recursive functions in Ruby the right way

Have you ever wondered why it is not so easy to calculate the factoria of number 100000 in Ruby

Factorial calculation is a classical problem, when we are talking about the recursions in the Ruby programing language. Can one use Ruby to calculate the factoria of 1000! - absolutely Here is a simple example of the factorial function in Ruby:

def fact(n)
  return 1 if n <= 1
  n * fact(n - 1)
end

Let’s run it:

puts fact(5)
120
=> nil

Easy right!

Now let’s do it for some slightly bigger number:

puts fact(100000)
(irb):3:in 'Object#fact': stack level too deep (SystemStackError)

Wow! That was not really expected! So what happened?

To understand the problem better, let’s take a look at the sequence of operations that would happen if we want to calculate previous fact(5):

puts fact(5)

#=> 5 * fact(4)
#=> 5 * (4 * fact(3))
#=> 5 * (4 * (3 * fact(2))
#=> 5 * (4 * (3 * (2 * fact(1)))
#=> 5 * (4 * (3 * (2 * 1)))
#=> 5 * (4 * (3 * 2))
#=> 5 * (4 * 6)
#=> 5 * 24
#=> 120

From the example above it is obvious that operations keep stacking up with each recursive call to fact function. First the Ruby interpreter is expanding to the very end of the recursion to return 1 to satisfy the return condition at n <= 1, and second it reduces expression to a single calculated value in the end. Since during the recursive call the interpreter keeps track of all the previous values, it will keep all these intermediate values in the stack and since the stack size is limited, it will eventually on the big numbers like 100000 cause a SystemStackError, or stack overflow.

Tail recursion

There is a better way how the previous fact function can be rewritten to pass the intermediate value inside parameters argument of the function call:


def fact(n, memo=1)
  return memo if n <= 1
  fact(n-1, n * memo)
end

Please note that we have to initialize the starting value of the memo to 1 so that it will be correctly multiplied in next iterations, otherwise the value of the memo should be set during the function call like fact(5, 1)

Now let’s look at the sequence of operations for the newly modified function fact:

puts fact(5)

#=> fact(4, 5)
#=> fact(3, 20)
#=> fact(2, 60)
#=> fact(1, 120)
#=> 120

Much cleaner right, but also we achieved another effect and now we can call this new refined fact function a special tail-recursive function. A function is considered a tail-recursive if the recursive call is the very last operation performed before the function returns its result. This means that after the recursive call returns, no further computation or operations are needed within the current function’s scope.

Now let’s try to calculate the fact(100000) with new tail-recursive variant of the function:

puts fact(100000)
(irb):16:in 'Object#fact': stack level too deep (SystemStackError)

This will still cause the SystemStackError, but why?

It turns out that that the Ruby interpreter needs to keep track of where to return after each execution of the function. Each recursive call to the fact method gets added to the call stack.

Tail call optimization (TCO) in Ruby

Ruby has a way to tell to the interpreter to optimize the tail-recursive call in the runtime. When tail call optimization takes place the tail recursion will work like a loop. Interpreter replaces each time the topmost stack frame with a new one, instead of stacking up the call stack.

By default tail call optimization is not enabled in the Ruby, but it can be enabled at runtime with a compile option:

RubyVM::InstructionSequence.compile_option = {
  tailcall_optimization: true,
  trace_instruction: false
}

def fact(n, memo=1)
  return memo if n <= 1
  fact(n-1, n * memo)
end

puts fact(100000)

Now you will be able to get the calculated value of the 100000! and there should be no SystemStackError error.

Why TCO is not enabled by default in Ruby

Major problem with TCO is that it messes up the call stack and stack traces, which makes debugging harder, however if you know what you are doing, you can enable the TCO during the runtime and have all the power of the Tail call optimization (TCO) in Ruby.

Further reading

Congratulations 🏆 on learning about How to write recursive functions with TCO in Ruby.

Written by

Sergii Demianchuk

Sergii Demianchuk is a Senior Software Engineering Technical Leader in the S&TO organization at Cisco, where he drives innovation in application security and vulnerability management. With 16 years of specialized experience in AppSec, Sergii has made significant contributions to SBOM security analysis and vulnerability management frameworks. He actively shapes industry standards as a member of both the OASIS OpenEoX Technical Committee and OASIS CSAF Technical Committee. His active participation in the CVE community reflects his commitment to advancing cybersecurity intelligence and standardization across the industry.