tailrec

package
v2.0.3 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Dec 20, 2025 License: Apache-2.0 Imports: 2 Imported by: 0

Documentation

Overview

Package tailrec provides a trampoline implementation for tail-call optimization in Go.

Overview

Go does not support tail-call optimization (TCO) at the language level, which means deeply recursive functions can cause stack overflow errors. The trampoline pattern provides a way to convert recursive algorithms into iterative ones, avoiding stack overflow while maintaining the clarity of recursive code.

A trampoline works by returning instructions about what to do next instead of directly making recursive calls. The trampoline executor then interprets these instructions in a loop, effectively converting recursion into iteration.

Core Concepts

The package provides three main operations:

**Bounce**: Indicates that the computation should continue with a new value. This represents a recursive call in the original algorithm.

**Land**: Indicates that the computation is complete and returns a final result. This represents the base case in the original algorithm.

**Unwrap**: Extracts the state from a Trampoline, allowing the executor to determine whether to continue (Bounce) or terminate (Land).

Type Parameters

The Trampoline type has two type parameters:

  • B: The "bounce" type - the intermediate state passed between recursive steps
  • L: The "land" type - the final result type when computation completes

Basic Usage

Converting a recursive factorial function to use trampolines:

// Traditional recursive factorial (can overflow stack)
func factorial(n int) int {
    if n <= 1 {
        return 1
    }
    return n * factorial(n-1)
}

// Trampoline-based factorial (stack-safe)
type State struct {
    n   int
    acc int
}

func factorialStep(state State) tailrec.Trampoline[State, int] {
    if state.n <= 1 {
        return tailrec.Land[State](state.acc)
    }
    return tailrec.Bounce[int](State{state.n - 1, state.acc * state.n})
}

// Execute the trampoline
func factorial(n int) int {
    current := tailrec.Bounce[int](State{n, 1})
    for {
        bounce, land, isLand := tailrec.Unwrap(current)
        if isLand {
            return land
        }
        current = factorialStep(bounce)
    }
}

Fibonacci Example

Computing Fibonacci numbers with tail recursion:

type FibState struct {
    n    int
    curr int
    prev int
}

func fibStep(state FibState) tailrec.Trampoline[FibState, int] {
    if state.n <= 0 {
        return tailrec.Land[FibState](state.curr)
    }
    return tailrec.Bounce[int](FibState{
        n:    state.n - 1,
        curr: state.prev + state.curr,
        prev: state.curr,
    })
}

func fibonacci(n int) int {
    current := tailrec.Bounce[int](FibState{n, 1, 0})
    for {
        bounce, land, isLand := tailrec.Unwrap(current)
        if isLand {
            return land
        }
        current = fibStep(bounce)
    }
}

List Processing Example

Summing a list with tail recursion:

type SumState struct {
    list []int
    sum  int
}

func sumStep(state SumState) tailrec.Trampoline[SumState, int] {
    if len(state.list) == 0 {
        return tailrec.Land[SumState](state.sum)
    }
    return tailrec.Bounce[int](SumState{
        list: state.list[1:],
        sum:  state.sum + state.list[0],
    })
}

func sumList(list []int) int {
    current := tailrec.Bounce[int](SumState{list, 0})
    for {
        bounce, land, isLand := tailrec.Unwrap(current)
        if isLand {
            return land
        }
        current = sumStep(bounce)
    }
}

Integration with Reader Monads

The tailrec package is commonly used with Reader monads (readerio, context/readerio) to implement stack-safe recursive computations that also depend on an environment:

import (
    "github.com/IBM/fp-go/v2/readerio"
    "github.com/IBM/fp-go/v2/tailrec"
)

type Env struct {
    Multiplier int
}

func compute(n int) readerio.ReaderIO[Env, int] {
    return readerio.TailRec(
        n,
        func(n int) readerio.ReaderIO[Env, tailrec.Trampoline[int, int]] {
            return func(env Env) func() tailrec.Trampoline[int, int] {
                return func() tailrec.Trampoline[int, int] {
                    if n <= 0 {
                        return tailrec.Land[int](n * env.Multiplier)
                    }
                    return tailrec.Bounce[int](n - 1)
                }
            }
        },
    )
}

Benefits

  • **Stack Safety**: Prevents stack overflow for deep recursion
  • **Clarity**: Maintains the structure of recursive algorithms
  • **Performance**: Converts recursion to iteration without manual rewriting
  • **Composability**: Works well with functional programming patterns

When to Use

Use trampolines when:

  • You have a naturally recursive algorithm
  • The recursion depth could be large (thousands of calls)
  • You want to maintain the clarity of recursive code
  • You're working with functional programming patterns

Performance Considerations

While trampolines prevent stack overflow, they do have some overhead:

  • Each step allocates a Trampoline struct
  • The executor loop adds some indirection

For shallow recursion (< 1000 calls), direct recursion may be faster. For deep recursion, trampolines are essential to avoid stack overflow.

Key Functions

**Bounce**: Create a trampoline that continues computation with a new state

**Land**: Create a trampoline that terminates with a final result

**Unwrap**: Extract the state and determine if computation should continue

See Also

  • readerio.TailRec: Tail-recursive Reader IO computations
  • context/readerio.TailRec: Tail-recursive Reader IO with context
Example (Collatz)

Example_collatz demonstrates the Collatz conjecture using trampolines.

package main

import (
	"fmt"

	"github.com/IBM/fp-go/v2/tailrec"
)

func main() {
	// Define the step function
	collatzStep := func(n int) tailrec.Trampoline[int, int] {
		if n <= 1 {
			return tailrec.Land[int](n)
		}
		if n%2 == 0 {
			return tailrec.Bounce[int](n / 2)
		}
		return tailrec.Bounce[int](3*n + 1)
	}

	// Execute the trampoline and count steps
	collatzSteps := func(n int) int {
		current := tailrec.Bounce[int](n)
		steps := 0
		for {
			if current.Landed {
				return steps
			}
			current = collatzStep(current.Bounce)
			steps++
		}
	}

	// Count steps for Collatz sequence starting at 27
	result := collatzSteps(27)
	fmt.Printf("Collatz(27) takes %d steps to reach 1\n", result)

}
Output:

Collatz(27) takes 112 steps to reach 1
Example (Countdown)

Example_countdown demonstrates a simple countdown using trampolines.

package main

import (
	"fmt"

	"github.com/IBM/fp-go/v2/tailrec"
)

func main() {
	// Define the step function
	countdownStep := func(n int) tailrec.Trampoline[int, int] {
		if n <= 0 {
			return tailrec.Land[int](0)
		}
		return tailrec.Bounce[int](n - 1)
	}

	// Execute the trampoline
	countdown := func(n int) int {
		current := tailrec.Bounce[int](n)
		for {
			if current.Landed {
				return current.Land
			}
			current = countdownStep(current.Bounce)
		}
	}

	// Countdown from 5
	result := countdown(5)
	fmt.Printf("countdown(5) = %d\n", result)

}
Output:

countdown(5) = 0
Example (DeepRecursion)

Example_deepRecursion demonstrates handling deep recursion without stack overflow.

package main

import (
	"fmt"

	"github.com/IBM/fp-go/v2/tailrec"
)

func main() {
	// Define the step function
	countdownStep := func(n int) tailrec.Trampoline[int, int] {
		if n <= 0 {
			return tailrec.Land[int](0)
		}
		return tailrec.Bounce[int](n - 1)
	}

	// Execute the trampoline
	countdown := func(n int) int {
		current := tailrec.Bounce[int](n)
		for {
			if current.Landed {
				return current.Land
			}
			current = countdownStep(current.Bounce)
		}
	}

	// This would cause stack overflow with regular recursion
	// but works fine with trampolines
	result := countdown(100000)
	fmt.Printf("countdown(100000) = %d (no stack overflow!)\n", result)

}
Output:

countdown(100000) = 0 (no stack overflow!)
Example (Factorial)

Example_factorial demonstrates implementing factorial using trampolines.

package main

import (
	"fmt"

	"github.com/IBM/fp-go/v2/tailrec"
)

func main() {
	type State struct {
		n   int
		acc int
	}

	// Define the step function
	factorialStep := func(state State) tailrec.Trampoline[State, int] {
		if state.n <= 1 {
			return tailrec.Land[State](state.acc)
		}
		return tailrec.Bounce[int](State{state.n - 1, state.acc * state.n})
	}

	// Execute the trampoline
	factorial := func(n int) int {
		current := tailrec.Bounce[int](State{n, 1})
		for {
			if current.Landed {
				return current.Land
			}
			current = factorialStep(current.Bounce)
		}
	}

	// Calculate factorial of 5
	result := factorial(5)
	fmt.Printf("5! = %d\n", result)

}
Output:

5! = 120
Example (Fibonacci)

Example_fibonacci demonstrates computing Fibonacci numbers using trampolines.

package main

import (
	"fmt"

	"github.com/IBM/fp-go/v2/tailrec"
)

func main() {
	type State struct {
		n    int
		curr int
		prev int
	}

	// Define the step function
	fibStep := func(state State) tailrec.Trampoline[State, int] {
		if state.n <= 0 {
			return tailrec.Land[State](state.curr)
		}
		return tailrec.Bounce[int](State{
			n:    state.n - 1,
			curr: state.prev + state.curr,
			prev: state.curr,
		})
	}

	// Execute the trampoline
	fibonacci := func(n int) int {
		current := tailrec.Bounce[int](State{n, 1, 0})
		for {
			if current.Landed {
				return current.Land
			}
			current = fibStep(current.Bounce)
		}
	}

	// Calculate 10th Fibonacci number
	result := fibonacci(10)
	fmt.Printf("fib(10) = %d\n", result)

}
Output:

fib(10) = 89
Example (FieldAccess)

Example_fieldAccess demonstrates accessing trampoline fields directly.

package main

import (
	"fmt"

	"github.com/IBM/fp-go/v2/tailrec"
)

func main() {
	// Create a bounce trampoline
	bounceTramp := tailrec.Bounce[string](42)
	fmt.Printf("Bounce: value=%d, landed=%v\n", bounceTramp.Bounce, bounceTramp.Landed)

	// Create a land trampoline
	landTramp := tailrec.Land[int]("result")
	fmt.Printf("Land: value=%s, landed=%v\n", landTramp.Land, landTramp.Landed)

}
Output:

Bounce: value=42, landed=false
Land: value=result, landed=true
Example (Gcd)

Example_gcd demonstrates computing greatest common divisor using trampolines.

package main

import (
	"fmt"

	"github.com/IBM/fp-go/v2/tailrec"
)

func main() {
	type State struct {
		a int
		b int
	}

	// Define the step function (Euclidean algorithm)
	gcdStep := func(state State) tailrec.Trampoline[State, int] {
		if state.b == 0 {
			return tailrec.Land[State](state.a)
		}
		return tailrec.Bounce[int](State{state.b, state.a % state.b})
	}

	// Execute the trampoline
	gcd := func(a, b int) int {
		current := tailrec.Bounce[int](State{a, b})
		for {
			if current.Landed {
				return current.Land
			}
			current = gcdStep(current.Bounce)
		}
	}

	// Calculate GCD of 48 and 18
	result := gcd(48, 18)
	fmt.Printf("gcd(48, 18) = %d\n", result)

}
Output:

gcd(48, 18) = 6
Example (SumList)

Example_sumList demonstrates processing a list using trampolines.

package main

import (
	"fmt"

	"github.com/IBM/fp-go/v2/tailrec"
)

func main() {
	type State struct {
		list []int
		sum  int
	}

	// Define the step function
	sumStep := func(state State) tailrec.Trampoline[State, int] {
		if len(state.list) == 0 {
			return tailrec.Land[State](state.sum)
		}
		return tailrec.Bounce[int](State{
			list: state.list[1:],
			sum:  state.sum + state.list[0],
		})
	}

	// Execute the trampoline
	sumList := func(list []int) int {
		current := tailrec.Bounce[int](State{list, 0})
		for {
			if current.Landed {
				return current.Land
			}
			current = sumStep(current.Bounce)
		}
	}

	// Sum a list of numbers
	numbers := []int{1, 2, 3, 4, 5}
	result := sumList(numbers)
	fmt.Printf("sum([1,2,3,4,5]) = %d\n", result)

}
Output:

sum([1,2,3,4,5]) = 15

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type Trampoline

type Trampoline[B, L any] struct {
	// Land holds the final result value when the computation has completed.
	// This field is only meaningful when Landed is true.
	Land L

	// Bounce holds the intermediate state for the next recursive step.
	// This field is only meaningful when Landed is false.
	Bounce B

	// Landed indicates whether the computation has completed.
	// When true, the Land field contains the final result.
	// When false, the Bounce field contains the state for the next iteration.
	Landed bool
}

Trampoline represents a step in a tail-recursive computation.

A Trampoline can be in one of two states:

  • Bounce: The computation should continue with a new intermediate state (type B)
  • Land: The computation is complete with a final result (type L)

Type Parameters:

  • B: The "bounce" type - intermediate state passed between recursive steps
  • L: The "land" type - the final result type when computation completes

The trampoline pattern allows converting recursive algorithms into iterative ones, preventing stack overflow for deep recursion while maintaining code clarity.

Example:

// Factorial using trampolines
type State struct { n, acc int }

func factorialStep(state State) Trampoline[State, int] {
    if state.n <= 1 {
        return Land[State](state.acc)  // Base case
    }
    return Bounce[int](State{state.n - 1, state.acc * state.n})  // Recursive case
}

See package documentation for more examples and usage patterns.

func Bounce

func Bounce[L, B any](b B) Trampoline[B, L]

Bounce creates a Trampoline that indicates the computation should continue with a new intermediate state.

This represents a recursive call in the original algorithm. The computation will continue by processing the provided state value in the next iteration.

Type Parameters:

  • L: The final result type (land type)
  • B: The intermediate state type (bounce type)

Parameters:

  • b: The new intermediate state to process in the next step

Returns:

  • A Trampoline in the "bounce" state containing the intermediate value

Example:

// Countdown that bounces until reaching zero
func countdownStep(n int) Trampoline[int, int] {
    if n <= 0 {
        return Land[int](0)
    }
    return Bounce[int](n - 1)  // Continue with n-1
}
Example

ExampleBounce demonstrates creating a trampoline that continues computation.

package main

import (
	"fmt"

	"github.com/IBM/fp-go/v2/tailrec"
)

func main() {
	// Create a bounce trampoline with value 42
	tramp := tailrec.Bounce[string](42)

	// Access fields directly to inspect the state
	fmt.Printf("Is computation complete? %v\n", tramp.Landed)
	fmt.Printf("Next value to process: %d\n", tramp.Bounce)

}
Output:

Is computation complete? false
Next value to process: 42

func Land

func Land[B, L any](l L) Trampoline[B, L]

Land creates a Trampoline that indicates the computation is complete with a final result.

This represents the base case in the original recursive algorithm. When a Land trampoline is encountered, the executor should stop iterating and return the final result.

Type Parameters:

  • B: The intermediate state type (bounce type)
  • L: The final result type (land type)

Parameters:

  • l: The final result value

Returns:

  • A Trampoline in the "land" state containing the final result

Example:

// Factorial base case
func factorialStep(state State) Trampoline[State, int] {
    if state.n <= 1 {
        return Land[State](state.acc)  // Computation complete
    }
    return Bounce[int](State{state.n - 1, state.acc * state.n})
}
Example

ExampleLand demonstrates creating a trampoline that completes computation.

package main

import (
	"fmt"

	"github.com/IBM/fp-go/v2/tailrec"
)

func main() {
	// Create a land trampoline with final result
	tramp := tailrec.Land[int]("done")

	// Access fields directly to inspect the state
	fmt.Printf("Is computation complete? %v\n", tramp.Landed)
	fmt.Printf("Final result: %s\n", tramp.Land)

}
Output:

Is computation complete? true
Final result: done

func (Trampoline[B, L]) Format

func (t Trampoline[B, L]) Format(f fmt.State, verb rune)

Format implements fmt.Formatter for Trampoline. Supports various formatting verbs for detailed output.

func (Trampoline[B, L]) GoString

func (t Trampoline[B, L]) GoString() string

GoString implements fmt.GoStringer for Trampoline. Returns a Go-syntax representation that could be used to recreate the value.

func (Trampoline[B, L]) LogValue

func (t Trampoline[B, L]) LogValue() slog.Value

LogValue implements the slog.LogValuer interface for Trampoline.

This method allows Trampoline values to be logged using Go's structured logging (log/slog) with proper representation of their state:

  • When Landed is true: returns a group with a single "landed" attribute containing the Land value
  • When Landed is false: returns a group with a single "bouncing" attribute containing the Bounce value

The implementation ensures that Trampoline values are logged in a structured, readable format that clearly shows the current state of the tail-recursive computation.

Example usage:

trampoline := tailrec.Bounce[int](42)
slog.Info("Processing", "state", trampoline)
// Logs: {"level":"info","msg":"Processing","state":{"bouncing":42}}

result := tailrec.Land[int](100)
slog.Info("Complete", "result", result)
// Logs: {"level":"info","msg":"Complete","result":{"landed":100}}

This is particularly useful for debugging tail-recursive computations and understanding the flow of recursive algorithms at runtime.

func (Trampoline[B, L]) String

func (t Trampoline[B, L]) String() string

String implements fmt.Stringer for Trampoline. Returns a human-readable string representation of the trampoline state.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL