Groovy trampoline() enables stack-safe recursive closures. 10+ tested examples covering factorial, Fibonacci, tree traversal, and tail-call optimization.
“Recursion is elegant until it blows the stack. Trampolining keeps the elegance and throws away the stack frames.”
Guy Steele, Lambda Papers
Last Updated: March 2026 | Tested on: Groovy 5.x, Java 17+ | Difficulty: Intermediate to Advanced | Reading Time: 18 minutes
Recursion makes code elegant and expressive, but it has a dirty secret on the JVM: every recursive call adds a frame to the call stack, and the stack has a finite size. Call a function deep enough and you get the dreaded StackOverflowError. The groovy trampoline method solves this by converting recursive closures into iterative loops that reuse a single stack frame.
Some languages solve this with tail-call optimization (TCO) – the compiler detects that the last thing a function does is call itself, and reuses the current stack frame instead of creating a new one. But the JVM doesn’t support TCO natively. Neither does Java. And by extension, neither does Groovy – at least not automatically.
Enter .trampoline(). This Groovy closure method gives you stack-safe recursion by converting recursive calls into an iterative loop internally. You write recursive code, but it executes iteratively – no stack overflow, no matter how deep the recursion goes.
In this post, we’ll explore Groovy trampoline with 10+ tested examples, from simple factorial to mutual recursion, tree traversal, and state machines. If you’re new to closures, start with our Groovy Closures Complete Guide. If you want to combine trampolining with caching, check out our Groovy Memoization post.
Let’s make recursion safe.
Table of Contents
The Stack Overflow Problem
Before we look at the solution, let’s understand the problem clearly.
Every time a function calls itself recursively, the JVM pushes a new frame onto the call stack. This frame holds local variables, the return address, and other bookkeeping data. The default JVM stack size is typically around 512KB to 1MB, which allows roughly 5,000 to 15,000 recursive calls before you get a StackOverflowError.
For many problems, that’s not enough. Computing the 100,000th number in a sequence, traversing a deeply nested tree, or processing a long linked list recursively will all blow the stack. You could increase the stack size with -Xss, but that’s a band-aid, not a solution.
According to the official Groovy closures documentation, the trampoline() method solves this by converting tail-recursive closures into iterative execution. The recursive call doesn’t actually recurse – it returns a special TrampolineClosure object that the trampoline loop evaluates iteratively.
How Trampoline Works
The trampoline pattern works in three steps:
- You call
.trampoline()on a closure – this wraps it in aTrampolineClosure - Inside the closure, recursive calls return a thunk – instead of actually recursing, the closure returns a deferred computation
- The trampoline loop evaluates thunks iteratively – it keeps calling the returned thunks in a while loop until a non-thunk value is returned (the final result)
Think of it like a real trampoline: instead of stacking function calls on top of each other (which eventually topples), each call “bounces” back to the ground level before the next call. The stack never grows beyond a single frame.
Key requirement: Your closure must be tail-recursive – the recursive call must be the very last operation in the closure. If you do something with the result after the recursive call (like n * factorial(n-1)), it’s not tail-recursive and won’t work correctly with trampoline.
10 Practical Examples
Example 1: The Stack Overflow Problem Demonstrated
What we’re doing: First showing how regular recursion fails, then fixing it with trampoline.
Example 1: Stack Overflow vs Trampoline
// Regular recursive closure - will blow the stack
def countdownRegular
countdownRegular = { int n ->
if (n == 0) return "Done!"
countdownRegular(n - 1) // Direct recursive call
}
// Works for small values
println "Regular countdown(100): ${countdownRegular(100)}"
// Fails for large values
try {
countdownRegular(100000)
} catch (StackOverflowError e) {
println "Regular countdown(100000): StackOverflowError!"
}
// Trampoline version - stack-safe!
def countdownTrampoline
countdownTrampoline = { int n ->
if (n == 0) return "Done!"
countdownTrampoline.trampoline(n - 1) // Returns a thunk, doesn't recurse
}.trampoline()
// Works for ANY value - no stack overflow
println "Trampoline countdown(100): ${countdownTrampoline(100)}"
println "Trampoline countdown(100000): ${countdownTrampoline(100000)}"
println "Trampoline countdown(1000000): ${countdownTrampoline(1000000)}"
Output
Regular countdown(100): Done! Regular countdown(100000): StackOverflowError! Trampoline countdown(100): Done! Trampoline countdown(100000): Done! Trampoline countdown(1000000): Done!
What happened here: The regular recursive closure blows the stack at around 100,000 calls. The trampoline version handles 1,000,000 calls without breaking a sweat. Notice the two changes: (1) we call .trampoline() on the closure definition, and (2) inside the closure, we call countdownTrampoline.trampoline(n - 1) instead of countdownTrampoline(n - 1). The .trampoline() call inside returns a thunk (a deferred computation) instead of actually recursing. The outer trampoline loop then evaluates these thunks iteratively.
Example 2: Tail-Recursive Factorial
What we’re doing: Converting the classic factorial function to tail-recursive form and using trampoline to make it stack-safe.
Example 2: Factorial with Trampoline
// WRONG: This is NOT tail-recursive
// The multiplication happens AFTER the recursive call returns
def factorialBad = { int n ->
if (n <= 1) return 1G // BigInteger
n * call(n - 1) // NOT tail-recursive: n * (recursive result)
}
// RIGHT: Tail-recursive with accumulator
def factorialTail
factorialTail = { BigInteger n, BigInteger acc = 1G ->
if (n <= 1) return acc
factorialTail.trampoline(n - 1, n * acc) // Tail position!
}.trampoline()
// Test with small values
println "factorial(5) = ${factorialTail(5)}"
println "factorial(10) = ${factorialTail(10)}"
println "factorial(20) = ${factorialTail(20)}"
// Test with large values - no stack overflow!
println "factorial(100) = ${factorialTail(100)}"
println "factorial(1000) has ${factorialTail(1000).toString().size()} digits"
println "factorial(10000) has ${factorialTail(10000).toString().size()} digits"
println "factorial(50000) has ${factorialTail(50000).toString().size()} digits"
Output
factorial(5) = 120 factorial(10) = 3628800 factorial(20) = 2432902008176640000 factorial(100) = 93326215443944152681699238856266700490715968264381621468592963895217599993229915608941463976156518286253697920827223758251185210916864000000000000000000000000 factorial(1000) has 2568 digits factorial(10000) has 35660 digits factorial(50000) has 213237 digits
What happened here: The key transformation is adding an accumulator parameter. Instead of n * factorial(n-1) (which needs the result before multiplying), we pass the accumulated result forward: factorialTail(n-1, n * acc). The recursive call is now the absolute last thing that happens – no operation depends on its return value. This is what makes it tail-recursive and compatible with trampoline. We computed factorial of 50,000 (a number with over 200,000 digits) without any stack issues.
Example 3: Sum of a Range with Trampoline
What we’re doing: Summing a large range of numbers recursively – a simple example to solidify the accumulator pattern.
Example 3: Recursive Sum
// Tail-recursive sum with trampoline
def sumRange
sumRange = { long n, long acc = 0 ->
if (n == 0) return acc
sumRange.trampoline(n - 1, acc + n)
}.trampoline()
// Small values
println "sum(10) = ${sumRange(10)}"
println "sum(100) = ${sumRange(100)}"
println "sum(1000) = ${sumRange(1000)}"
// Large values - would crash without trampoline
println "sum(100000) = ${sumRange(100000)}"
println "sum(500000) = ${sumRange(500000)}"
// Verify against formula: n*(n+1)/2
def n = 100000L
assert sumRange(n) == n * (n + 1) / 2
println "\nVerified: sumRange(${n}) == ${n}*(${n}+1)/2 = ${n * (n + 1) / 2}"
// Tail-recursive product (similar pattern)
def productRange
productRange = { long from, long to, BigInteger acc = 1G ->
if (from > to) return acc
productRange.trampoline(from + 1, to, acc * from)
}.trampoline()
println "\nproduct(1..10) = ${productRange(1, 10)}"
println "product(1..20) = ${productRange(1, 20)}"
Output
sum(10) = 55 sum(100) = 5050 sum(1000) = 500500 sum(100000) = 5000050000 sum(500000) = 125000250000 Verified: sumRange(100000) == 100000*(100000+1)/2 = 5000050000 product(1..10) = 3628800 product(1..20) = 2432902008176640000
What happened here: The pattern is always the same – pass the accumulated result as a parameter and make the recursive call the last operation. For sum, we pass acc + n forward. For product, we pass acc * from forward. The trampoline converts what would be 500,000 stack frames into a simple while loop. We verified the sum against the mathematical formula to confirm correctness.
Example 4: Fibonacci with Trampoline
What we’re doing: Computing Fibonacci numbers with trampoline – requires a clever two-accumulator approach since Fibonacci needs two previous values.
Example 4: Fibonacci with Trampoline
// Tail-recursive Fibonacci with two accumulators
def fibTrampoline
fibTrampoline = { long n, BigInteger a = 0G, BigInteger b = 1G ->
if (n == 0) return a
if (n == 1) return b
fibTrampoline.trampoline(n - 1, b, a + b)
}.trampoline()
// Standard Fibonacci values
println "=== Fibonacci Sequence ==="
(0..15).each { print "${fibTrampoline(it)} " }
println ""
// Large Fibonacci numbers - no stack overflow
println "\n=== Large Fibonacci ==="
println "fib(100) = ${fibTrampoline(100)}"
println "fib(1000) = ${fibTrampoline(1000).toString().take(30)}... (${fibTrampoline(1000).toString().size()} digits)"
println "fib(10000) has ${fibTrampoline(10000).toString().size()} digits"
println "fib(50000) has ${fibTrampoline(50000).toString().size()} digits"
// Verify known values
assert fibTrampoline(10) == 55
assert fibTrampoline(20) == 6765
assert fibTrampoline(50) == 12586269025G
println "\nAll known values verified!"
Output
=== Fibonacci Sequence === 0 1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 === Large Fibonacci === fib(100) = 354224848179261915075 fib(1000) = 434665576869374564356885... (209 digits) fib(10000) has 2090 digits fib(50000) has 10450 digits All known values verified!
What happened here: The trick with Fibonacci is using two accumulators – a (current value) and b (next value). Each step shifts the window: a becomes b, and b becomes a + b. This avoids the classic double-recursion problem entirely. We computed fib(50000) – a number with over 10,000 digits – without any stack issues. Compare this with the memoized approach from our memoization post – memoization caches intermediate results but still uses the stack, while trampoline avoids the stack entirely.
Example 5: String Processing with Trampoline
What we’re doing: Using trampoline for recursive string operations – reversing, counting, and pattern matching on very long strings.
Example 5: String Processing
// Tail-recursive string reversal
def reverseString
reverseString = { String str, String acc = '' ->
if (str.isEmpty()) return acc
reverseString.trampoline(str.substring(1), str[0] + acc)
}.trampoline()
println "reverse('Groovy') = ${reverseString('Groovy')}"
println "reverse('trampoline') = ${reverseString('trampoline')}"
// Tail-recursive character counting
def countChar
countChar = { String str, char target, int acc = 0 ->
if (str.isEmpty()) return acc
def newAcc = str[0] == target ? acc + 1 : acc
countChar.trampoline(str.substring(1), target, newAcc)
}.trampoline()
def testStr = "mississippi"
println "\nCount 's' in '${testStr}': ${countChar(testStr, 's' as char)}"
println "Count 'i' in '${testStr}': ${countChar(testStr, 'i' as char)}"
println "Count 'p' in '${testStr}': ${countChar(testStr, 'p' as char)}"
// Tail-recursive palindrome check
def isPalindrome
isPalindrome = { String str ->
if (str.size() <= 1) return true
if (str[0] != str[-1]) return false
isPalindrome.trampoline(str.substring(1, str.size() - 1))
}.trampoline()
['racecar', 'hello', 'madam', 'groovy', 'level', 'a'].each {
println "isPalindrome('${it}'): ${isPalindrome(it)}"
}
// Test with a very long palindrome
def longPalindrome = 'a' * 50000 + 'b' + 'a' * 50000
println "\nLong palindrome (100,001 chars): ${isPalindrome(longPalindrome)}"
def notPalindrome = 'a' * 50000 + 'b' + 'a' * 49999 + 'c'
println "Long non-palindrome: ${isPalindrome(notPalindrome)}"
Output
reverse('Groovy') = yvoorG
reverse('trampoline') = enilopmart
Count 's' in 'mississippi': 4
Count 'i' in 'mississippi': 4
Count 'p' in 'mississippi': 2
isPalindrome('racecar'): true
isPalindrome('hello'): false
isPalindrome('madam'): true
isPalindrome('groovy'): false
isPalindrome('level'): true
isPalindrome('a'): true
Long palindrome (100,001 chars): true
Long non-palindrome: false
What happened here: All three string operations follow the tail-recursive pattern: process one character, pass the remainder and accumulated result forward. The palindrome check on a 100,001-character string would require 50,000 recursive calls – well beyond the stack limit for regular recursion, but trivial for trampoline. These examples show that Groovy trampoline isn’t just for math – it works for any recursive algorithm that can be written in tail position.
Example 6: Mutual Recursion with Trampoline
What we’re doing: Using trampoline with two closures that call each other – mutual recursion is one of trampoline’s most powerful use cases.
Example 6: Mutual Recursion
// isEven and isOdd defined in terms of each other
def isEven
def isOdd
isEven = { int n ->
if (n == 0) return true
isOdd.trampoline(n - 1)
}.trampoline()
isOdd = { int n ->
if (n == 0) return false
isEven.trampoline(n - 1)
}.trampoline()
// Small values
println "isEven(0) = ${isEven(0)}"
println "isEven(1) = ${isEven(1)}"
println "isEven(4) = ${isEven(4)}"
println "isOdd(7) = ${isOdd(7)}"
// Large values - mutual recursion that would overflow the stack
println "\nisEven(100000) = ${isEven(100000)}"
println "isOdd(100001) = ${isOdd(100001)}"
println "isEven(99999) = ${isEven(99999)}"
// More practical mutual recursion: parsing nested expressions
// Simulates parsing alternating patterns: A -> B -> A -> B -> done
def parseA
def parseB
parseA = { List tokens, List result = [] ->
if (tokens.isEmpty()) return result
if (tokens[0] == 'A') {
parseB.trampoline(tokens.tail(), result + ['Found A'])
} else {
result + ["Unexpected: ${tokens[0]}"]
}
}.trampoline()
parseB = { List tokens, List result = [] ->
if (tokens.isEmpty()) return result
if (tokens[0] == 'B') {
parseA.trampoline(tokens.tail(), result + ['Found B'])
} else {
result + ["Unexpected: ${tokens[0]}"]
}
}.trampoline()
def tokens = ['A', 'B', 'A', 'B', 'A', 'B']
println "\nParsing ${tokens}: ${parseA(tokens)}"
Output
isEven(0) = true isEven(1) = false isEven(4) = true isOdd(7) = true isEven(100000) = true isOdd(100001) = true isEven(99999) = false Parsing [A, B, A, B, A, B]: [Found A, Found B, Found A, Found B, Found A, Found B]
What happened here: Mutual recursion is when two functions call each other – isEven calls isOdd, and isOdd calls isEven. Without trampoline, this would blow the stack for large numbers (100,000 mutual calls). With trampoline on both closures, the bouncing between them happens iteratively. The parser example shows a more practical use – processing alternating tokens where two parsing functions hand off to each other. This pattern appears in real parsers, state machines, and protocol handlers.
Example 7: List Processing with Trampoline
What we’re doing: Processing large lists recursively – filtering, mapping, and folding with trampoline.
Example 7: List Processing
// Tail-recursive list filter
def filterList
filterList = { List items, Closure predicate, List acc = [] ->
if (items.isEmpty()) return acc
def newAcc = predicate(items[0]) ? acc + items[0] : acc
filterList.trampoline(items.tail(), predicate, newAcc)
}.trampoline()
def numbers = (1..100).toList()
def evens = filterList(numbers, { it % 2 == 0 })
println "Evens (first 10): ${evens.take(10)}"
println "Even count: ${evens.size()}"
// Tail-recursive list map
def mapList
mapList = { List items, Closure transform, List acc = [] ->
if (items.isEmpty()) return acc
mapList.trampoline(items.tail(), transform, acc + transform(items[0]))
}.trampoline()
def doubled = mapList((1..10).toList(), { it * 2 })
println "\nDoubled: ${doubled}"
// Tail-recursive reduce (fold left)
def foldLeft
foldLeft = { List items, initial, Closure operation ->
if (items.isEmpty()) return initial
foldLeft.trampoline(items.tail(), operation(initial, items[0]), operation)
}.trampoline()
def sum = foldLeft((1..100).toList(), 0, { acc, val -> acc + val })
println "Sum 1..100: ${sum}"
def product = foldLeft((1..10).toList(), 1G, { acc, val -> acc * val })
println "Product 1..10: ${product}"
// Large list processing - would stack overflow without trampoline
def largeList = (1..50000).toList()
def largeSum = foldLeft(largeList, 0L, { acc, val -> acc + val })
println "\nSum 1..50000: ${largeSum}"
println "Verified: ${largeSum == 50000L * 50001L / 2}"
Output
Evens (first 10): [2, 4, 6, 8, 10, 12, 14, 16, 18, 20] Even count: 50 Doubled: [2, 4, 6, 8, 10, 12, 14, 16, 18, 20] Sum 1..100: 5050 Product 1..10: 3628800 Sum 1..50000: 1250025000 Verified: true
What happened here: We reimplemented findAll, collect, and inject using tail-recursive trampoline closures. In practice, you’d use Groovy’s built-in methods for these operations since they’re already iterative. But this example demonstrates the pattern: any list-processing recursion can be made stack-safe with an accumulator and trampoline. The fold over 50,000 elements works without issues, while the equivalent regular recursion would fail around 10,000-15,000 elements.
Example 8: GCD and Number Theory with Trampoline
What we’re doing: Computing Greatest Common Divisor and other number-theoretic functions that are naturally recursive.
Example 8: GCD with Trampoline
// Euclidean algorithm - naturally tail-recursive!
def gcd
gcd = { BigInteger a, BigInteger b ->
if (b == 0) return a
gcd.trampoline(b, a % b)
}.trampoline()
println "gcd(48, 18) = ${gcd(48, 18)}"
println "gcd(100, 75) = ${gcd(100, 75)}"
println "gcd(1071, 462) = ${gcd(1071, 462)}"
// LCM using GCD
def lcm = { BigInteger a, BigInteger b -> (a * b).abs().intdiv(gcd(a, b)) }
println "\nlcm(12, 18) = ${lcm(12, 18)}"
println "lcm(7, 13) = ${lcm(7, 13)}"
// Very large numbers - GCD still works!
def huge1 = 123456789012345678901234567890G
def huge2 = 987654321098765432109876543210G
println "\ngcd(huge1, huge2) = ${gcd(huge1, huge2)}"
// Collatz conjecture: for any n, repeatedly apply:
// if even: n/2
// if odd: 3n+1
// Eventually reaches 1 (unproven but true for all tested numbers)
def collatzLengthImpl
collatzLengthImpl = { long n, int steps = 0 ->
if (n == 1) return steps
if (n % 2 == 0) {
collatzLengthImpl.trampoline((long)(n / 2), steps + 1)
} else {
collatzLengthImpl.trampoline((long)(3 * n + 1), steps + 1)
}
}
def collatzLength = collatzLengthImpl.trampoline()
println "\n=== Collatz Sequence Lengths ==="
[1, 2, 3, 7, 27, 100, 1000, 10000, 77031].each { n ->
println " collatz(${String.valueOf(n).padRight(6)}) takes ${collatzLength(n)} steps"
}
Output
gcd(48, 18) = 6 gcd(100, 75) = 25 gcd(1071, 462) = 21 lcm(12, 18) = 36 lcm(7, 13) = 91 gcd(huge1, huge2) = 9000000000900000000090 === Collatz Sequence Lengths === collatz(1 ) takes 0 steps collatz(2 ) takes 1 steps collatz(3 ) takes 7 steps collatz(7 ) takes 16 steps collatz(27 ) takes 111 steps collatz(100 ) takes 25 steps collatz(1000 ) takes 111 steps collatz(10000 ) takes 29 steps collatz(77031 ) takes 350 steps
What happened here: The Euclidean algorithm for GCD is a perfect fit for trampoline – it’s already tail-recursive by nature. The Collatz sequence is particularly interesting because the number of steps is unpredictable. The number 77031 takes 350 steps before reaching 1, with intermediate values that can be extremely large. Without trampoline, such deep recursion would crash. The GCD calculation on 30-digit numbers demonstrates that trampoline handles BigInteger arithmetic without any issues.
Example 9: State Machine with Trampoline
What we’re doing: Implementing a simple state machine where each state is a trampoline closure that transitions to the next state.
Example 9: State Machine
// State machine for processing a simple order workflow
def stateNew
def statePaid
def stateShipped
def stateDelivered
stateNew = { Map order, List log = [] ->
log = log + "Order #${order.id}: Created"
if (order.payment) {
statePaid.trampoline(order, log)
} else {
log + "STOPPED: No payment received"
}
}.trampoline()
statePaid = { Map order, List log ->
log = log + "Order #${order.id}: Payment received (\$${order.amount})"
if (order.warehouse) {
stateShipped.trampoline(order, log)
} else {
log + "STOPPED: Warehouse unavailable"
}
}.trampoline()
stateShipped = { Map order, List log ->
log = log + "Order #${order.id}: Shipped via ${order.carrier}"
if (order.delivered) {
stateDelivered.trampoline(order, log)
} else {
log + "IN TRANSIT: Awaiting delivery"
}
}.trampoline()
stateDelivered = { Map order, List log ->
log + "Order #${order.id}: Delivered! Complete."
}.trampoline()
// Test different order scenarios
println "=== Scenario 1: Full journey ==="
def order1 = [id: 101, payment: true, amount: 99.99,
warehouse: true, carrier: 'FedEx', delivered: true]
stateNew(order1).each { println " ${it}" }
println "\n=== Scenario 2: No payment ==="
def order2 = [id: 102, payment: false, amount: 49.99]
stateNew(order2).each { println " ${it}" }
println "\n=== Scenario 3: In transit ==="
def order3 = [id: 103, payment: true, amount: 149.99,
warehouse: true, carrier: 'UPS', delivered: false]
stateNew(order3).each { println " ${it}" }
println "\n=== Scenario 4: Warehouse issue ==="
def order4 = [id: 104, payment: true, amount: 29.99, warehouse: false]
stateNew(order4).each { println " ${it}" }
Output
=== Scenario 1: Full journey === Order #101: Created Order #101: Payment received ($99.99) Order #101: Shipped via FedEx Order #101: Delivered! Complete. === Scenario 2: No payment === Order #102: Created STOPPED: No payment received === Scenario 3: In transit === Order #103: Created Order #103: Payment received ($149.99) Order #103: Shipped via UPS IN TRANSIT: Awaiting delivery === Scenario 4: Warehouse issue === Order #104: Created Order #104: Payment received ($29.99) STOPPED: Warehouse unavailable
What happened here: Each state is a trampoline closure that either transitions to the next state or returns a final result. The log accumulator tracks the journey. This pattern is powerful for workflows, protocol handlers, and game logic where you have many states that transition between each other. Because each state uses .trampoline() for transitions, you could have workflows with thousands of state transitions without any stack concerns. It’s essentially a state machine implemented with mutual recursion – clean, readable, and stack-safe.
Example 10: Binary Search with Trampoline
What we’re doing: Implementing binary search as a tail-recursive trampoline closure – a practical algorithm that benefits from stack safety.
Example 10: Binary Search
// Tail-recursive binary search
def binarySearch
binarySearch = { List sortedList, target, int low = 0, int high = -1 ->
if (high == -1) high = sortedList.size() - 1
if (low > high) return -1 // Not found
int mid = (low + high).intdiv(2)
def midVal = sortedList[mid]
if (midVal == target) return mid
if (midVal < target) {
binarySearch.trampoline(sortedList, target, mid + 1, high)
} else {
binarySearch.trampoline(sortedList, target, low, mid - 1)
}
}.trampoline()
// Test with a sorted list
def data = (1..1000).toList()
println "=== Binary Search ==="
println "Find 500: index ${binarySearch(data, 500)}"
println "Find 1: index ${binarySearch(data, 1)}"
println "Find 1000: index ${binarySearch(data, 1000)}"
println "Find 999: index ${binarySearch(data, 999)}"
println "Find 1001: index ${binarySearch(data, 1001)} (not found)"
// Search in strings
def words = ['apple', 'banana', 'cherry', 'date', 'elderberry',
'fig', 'grape', 'honeydew', 'kiwi', 'lemon'].sort()
println "\n=== String Search ==="
println "Find 'grape': index ${binarySearch(words, 'grape')}"
println "Find 'mango': index ${binarySearch(words, 'mango')} (not found)"
println "Find 'apple': index ${binarySearch(words, 'apple')}"
// Tail-recursive power function (exponentiation by squaring)
def power
power = { BigInteger base, int exp, BigInteger acc = 1G ->
if (exp == 0) return acc
if (exp % 2 == 0) {
power.trampoline(base * base, exp.intdiv(2), acc)
} else {
power.trampoline(base, exp - 1, acc * base)
}
}.trampoline()
println "\n=== Fast Exponentiation ==="
println "2^10 = ${power(2, 10)}"
println "3^20 = ${power(3, 20)}"
println "2^100 = ${power(2, 100)}"
println "7^50 has ${power(7, 50).toString().size()} digits"
Output
=== Binary Search === Find 500: index 499 Find 1: index 0 Find 1000: index 999 Find 999: index 998 Find 1001: index -1 (not found) === String Search === Find 'grape': index 6 Find 'mango': index -1 (not found) Find 'apple': index 0 === Fast Exponentiation === 2^10 = 1024 3^20 = 3486784401 2^100 = 1267650600228229401496703205376 7^50 has 43 digits
What happened here: Binary search is naturally tail-recursive – each step either returns a result or recurses into one half of the array. While binary search’s O(log n) depth won’t overflow the stack for practical list sizes, the trampoline pattern ensures it’s safe for any size. The exponentiation-by-squaring algorithm is another beautiful fit – it computes base^exp in O(log n) steps, and the tail-recursive version with trampoline can handle enormous exponents without stack concerns.
Example 11 (Bonus): Flattening Deeply Nested Structures
What we’re doing: Recursively flattening a deeply nested list structure – a real-world scenario where regular recursion can fail.
Example 11: Deep Flattening
// Build a deeply nested structure
def buildNested
buildNested = { int depth, value ->
if (depth == 0) return value
[buildNested(depth - 1, value)]
}
// Create moderately nested structure for demo
def nested = buildNested(5, 'deep')
println "Nested 5 levels: ${nested}"
// Tail-recursive flatten using a work stack (iterative accumulation)
def flattenDeep
flattenDeep = { List items, List remaining = [], List acc = [] ->
if (items.isEmpty()) {
if (remaining.isEmpty()) return acc
flattenDeep.trampoline(remaining.head(), remaining.tail(), acc)
} else {
def first = items[0]
def rest = items.size() > 1 ? items.subList(1, items.size()) : []
if (first instanceof List) {
// Push rest onto remaining, process first
def newRemaining = rest.isEmpty() ? remaining : [rest] + remaining
flattenDeep.trampoline(first, newRemaining, acc)
} else {
flattenDeep.trampoline(rest, remaining, acc + first)
}
}
}.trampoline()
// Test flattening
def test1 = [[1, 2], [3, [4, 5]], [6]]
println "Flatten ${test1}: ${flattenDeep(test1)}"
def test2 = [1, [2, [3, [4, [5]]]]]
println "Flatten ${test2}: ${flattenDeep(test2)}"
def test3 = [[[['a']], [['b']]], [[['c']]]]
println "Flatten ${test3}: ${flattenDeep(test3)}"
// Empty and edge cases
println "Flatten []: ${flattenDeep([])}"
println "Flatten [[],[[]]]: ${flattenDeep([[], [[]]])}"
// Compare with Groovy's built-in flatten
assert flattenDeep(test1) == test1.flatten()
assert flattenDeep(test2) == test2.flatten()
assert flattenDeep(test3) == test3.flatten()
println "\nAll results match Groovy's built-in flatten()!"
Output
Nested 5 levels: [[[[[deep]]]]] Flatten [[1, 2], [3, [4, 5]], [6]]: [1, 2, 3, 4, 5, 6] Flatten [1, [2, [3, [4, [5]]]]]: [1, 2, 3, 4, 5] Flatten [[[[a]], [[b]]], [[[c]]]]: [a, b, c] Flatten []: [] Flatten [[],[[]]]: [] All results match Groovy's built-in flatten()!
What happened here: Flattening deeply nested structures is tricky to make tail-recursive because you need to process both the first element and the rest of the list. The solution uses a “remaining work” list as a stack – when we encounter a nested list, we push the rest of the current level onto the remaining stack and recurse into the nested list. This transforms what would be a tree-recursive problem into a linear, tail-recursive one. While Groovy’s built-in flatten() handles most cases, this trampoline version can handle arbitrarily deep nesting without stack overflow.
Trampoline vs Memoize – When to Use Which
Both trampoline and memoize are closure optimization techniques, but they solve completely different problems:
| Aspect | Trampoline | Memoize |
|---|---|---|
| Problem solved | Stack overflow from deep recursion | Redundant computation of same arguments |
| How it works | Converts recursion to iteration | Caches results in a map |
| Memory impact | Constant stack usage | Cache grows with unique arguments |
| Speed impact | Slight overhead per bounce | Massive speedup on repeated calls |
| Requirement | Must be tail-recursive | Must be a pure function |
| Best for | Deep recursion, large inputs | Expensive computations with repeated args |
Can you use both together? Yes! Memoize the expensive computation, and trampoline the recursion depth. For Fibonacci, memoization eliminates redundant subproblems while trampoline prevents stack overflow. In practice, though, once you make Fibonacci tail-recursive (with accumulators), it no longer has overlapping subproblems, so memoization isn’t needed. The choice depends on the structure of your specific algorithm.
Converting Recursion to Tail Recursion
The hardest part of using trampoline is converting your recursion to tail-recursive form. Here’s the recipe:
Step-by-Step Conversion
Converting to Tail Recursion
// STEP 1: Start with a regular recursive function
def sumListRegular = { List items ->
if (items.isEmpty()) return 0
items[0] + call(items.tail()) // NOT tail-recursive
}
// STEP 2: Add an accumulator parameter with a default value
def sumListTail = { List items, long acc = 0 ->
if (items.isEmpty()) return acc
call(items.tail(), acc + items[0]) // Tail-recursive!
}
// STEP 3: Apply trampoline
def sumListTrampoline
sumListTrampoline = { List items, long acc = 0 ->
if (items.isEmpty()) return acc
sumListTrampoline.trampoline(items.tail(), acc + items[0])
}.trampoline()
// All three produce the same result
def data = (1..100).toList()
println "Regular: ${sumListRegular(data)}"
println "Tail: ${sumListTail(data)}"
println "Trampoline: ${sumListTrampoline(data)}"
// The conversion pattern:
println """
=== Conversion Recipe ===
1. Identify the operation AFTER the recursive call (e.g., +, *, append)
2. Add an accumulator parameter (with default = identity value)
- For addition: acc = 0
- For multiplication: acc = 1
- For list building: acc = []
- For string building: acc = ''
3. Move the operation BEFORE the recursive call
- Instead of: items[0] + recurse(rest)
- Write: recurse(rest, acc + items[0])
4. In the base case, return the accumulator instead of the identity value
5. Apply .trampoline() to the closure and use .trampoline() for recursive calls
"""
Output
Regular: 5050 Tail: 5050 Trampoline: 5050 === Conversion Recipe === 1. Identify the operation AFTER the recursive call (e.g., +, *, append) 2. Add an accumulator parameter (with default = identity value) - For addition: acc = 0 - For multiplication: acc = 1 - For list building: acc = [] - For string building: acc = '' 3. Move the operation BEFORE the recursive call - Instead of: items[0] + recurse(rest) - Write: recurse(rest, acc + items[0]) 4. In the base case, return the accumulator instead of the identity value 5. Apply .trampoline() to the closure and use .trampoline() for recursive calls
This conversion recipe works for most single-recursion problems. For problems with branching recursion (like tree traversal where you visit both left and right children), you need to use a work stack or continuation-passing style, as we saw in Example 11.
Common Pitfalls
Pitfall 1: Non-Tail-Recursive Calls
Pitfall 1: Not Tail-Recursive
// BAD: This is NOT tail-recursive - multiplication happens AFTER the recursive call
def factorialBad
factorialBad = { int n ->
if (n <= 1) return 1G
n * factorialBad.trampoline(n - 1) // WON'T WORK CORRECTLY!
}.trampoline()
// This will produce wrong results or errors because
// n * trampoline(...) is NOT a tail call
// GOOD: Move the multiplication into the accumulator
def factorialGood
factorialGood = { int n, BigInteger acc = 1G ->
if (n <= 1) return acc
factorialGood.trampoline(n - 1, n * acc) // Tail call!
}.trampoline()
println "Correct factorial(10): ${factorialGood(10)}"
Output
Correct factorial(10): 3628800
Pitfall 2: Forgetting to Call .trampoline() on Both Sides
Pitfall 2: Missing Trampoline Calls
// BAD: Only outer .trampoline() - recursive calls still use the stack
def badCountdown
badCountdown = { int n ->
if (n == 0) return "Done"
badCountdown(n - 1) // Direct call - still recursive!
}.trampoline()
// GOOD: Both outer .trampoline() AND inner .trampoline()
def goodCountdown
goodCountdown = { int n ->
if (n == 0) return "Done"
goodCountdown.trampoline(n - 1) // Returns a thunk!
}.trampoline()
println "Good countdown(1000): ${goodCountdown(1000)}"
// Remember: closure.trampoline(args) inside = return a thunk
// closure.trampoline() on definition = wrap in trampoline loop
Output
Good countdown(1000): Done
Pitfall 3: Trampoline Overhead on Short Recursions
Pitfall 3: Overhead for Small Recursions
// For small recursion depths, trampoline adds overhead
def regularFact = { int n -> (1..n).inject(1G) { acc, val -> acc * val } }
def trampolineFact
trampolineFact = { int n, BigInteger acc = 1G ->
if (n <= 1) return acc
trampolineFact.trampoline(n - 1, n * acc)
}.trampoline()
// Benchmark
def start1 = System.nanoTime()
100000.times { regularFact(20) }
def time1 = (System.nanoTime() - start1) / 1_000_000
def start2 = System.nanoTime()
100000.times { trampolineFact(20) }
def time2 = (System.nanoTime() - start2) / 1_000_000
println "Regular (inject): ${time1}ms for 100K calls"
println "Trampoline: ${time2}ms for 100K calls"
println "\nRule: Use trampoline for DEEP recursion (1000+ levels)"
println " Use iteration for SHALLOW recursion"
Output
Regular (inject): 156ms for 100K calls
Trampoline: 298ms for 100K calls
Rule: Use trampoline for DEEP recursion (1000+ levels)
Use iteration for SHALLOW recursion
The trampoline has per-bounce overhead – creating thunk objects and the loop machinery. For shallow recursion (under 100 levels), this overhead outweighs the benefit. Reserve trampoline for algorithms where recursion depth is large or unpredictable.
Conclusion
We’ve explored Groovy trampoline from simple countdowns to mutual recursion, state machines, and dynamic programming. The .trampoline() method is Groovy’s answer to the JVM’s lack of tail-call optimization – it lets you write recursive code that executes iteratively, with constant stack usage regardless of recursion depth.
The pattern is always the same: make your closure tail-recursive (use accumulators), call .trampoline() on the closure definition, and use closureName.trampoline(args) for recursive calls inside. That’s it. Three changes and your recursion becomes stack-safe.
Combined with memoization for caching and delegation for flexible scoping, trampoline completes Groovy’s trio of closure optimization techniques.
Summary
.trampoline()converts tail-recursive closures into iterative execution – no stack overflow- Your closure must be tail-recursive – the recursive call must be the very last operation
- Use accumulators to convert regular recursion to tail recursion
- Trampoline works for mutual recursion (two closures calling each other)
- Use trampoline for deep recursion (1000+ levels); for shallow recursion, the overhead isn’t worth it
If you also work with build tools, CI/CD pipelines, or cloud CLIs, check out Command Playground to practice 105+ CLI tools directly in your browser — no install needed.
Up next: Groovy Elvis Operator – Default Values
Frequently Asked Questions
What is trampoline() in Groovy?
trampoline() is a method on Groovy closures that enables stack-safe recursion. It converts a tail-recursive closure into an iterative loop internally. Instead of each recursive call adding a frame to the call stack, the call returns a ‘thunk’ (deferred computation) that a trampoline loop evaluates iteratively. This means you can recurse millions of times without getting a StackOverflowError.
What is the difference between trampoline and memoize in Groovy?
They solve different problems. trampoline() prevents StackOverflowError by converting recursion to iteration – it doesn’t affect computation speed. memoize() caches results to avoid redundant computation – it doesn’t affect stack depth. trampoline requires tail-recursive closures, while memoize requires pure functions. You can sometimes use both together, though tail-recursive closures typically don’t have overlapping subproblems that would benefit from memoization.
Why does trampoline require tail recursion?
Trampoline works by replacing the recursive call with a thunk (a deferred computation object) that the trampoline loop evaluates. If the recursive call isn’t the last operation (e.g., n * recurse(n-1)), the multiplication needs the recursive result before it can complete – it can’t be deferred as a thunk. Only when the recursive call IS the last operation can it be safely replaced with a thunk. That’s why you need to convert to tail recursion using accumulators.
Does Groovy support tail-call optimization natively?
No. The JVM does not support tail-call optimization (TCO) at the bytecode level, so neither Java nor Groovy can optimize tail calls automatically. Groovy trampoline() method is a manual workaround that achieves the same effect – stack-safe recursion – through a different mechanism (converting recursion to iteration via thunks). Some other JVM languages like Scala have compiler-level TCO support via the @tailrec annotation.
Can I use trampoline with mutual recursion in Groovy?
Yes! Trampoline works perfectly with mutual recursion – when two or more closures call each other. Apply .trampoline() to each closure’s definition, and use closureName.trampoline(args) for each cross-closure call. The trampoline loop handles the bouncing between closures iteratively. This is particularly useful for state machines, parsers, and protocol handlers where multiple functions naturally call each other.
Related Posts
Previous in Series: Groovy Memoization – Cache Function Results
Next in Series: Groovy Elvis Operator – Default Values
Related Topics You Might Like:
- Groovy Closures – The Complete Guide
- Groovy Memoization – Cache Function Results
- Groovy Curry and Partial Application
This post is part of the Groovy & Grails Cookbook series on TechnoScripts.com

No comment