Learn the two hard recur rules: it must be in tail position, and it can only target the nearest function or loop.
recur is deliberately limited. It gives Clojure a stack-safe way to express loop-shaped recursion on the JVM, but only when the compiler can prove exactly where control should jump next.
recur in ClojureBefore diving into the limitations, let’s briefly revisit what recur does. recur performs a tail-position jump to the nearest function or loop, preventing stack growth for deep linear iteration.
Here’s a simple example of using recur in a function:
1(defn factorial [n]
2 (letfn [(fact-helper [acc n]
3 (if (zero? n)
4 acc
5 (recur (* acc n) (dec n))))]
6 (fact-helper 1 n)))
7
8;; Usage
9(factorial 5) ; => 120
In this example, recur is used to call fact-helper recursively, with the multiplication and decrement operations being the last actions performed.
recurOne of the primary limitations of recur is that it must be in the tail position. This means that recur must be the last operation in a function or loop. If recur is not in the tail position, Clojure will throw a compilation error.
Example of Incorrect Usage:
1(defn sum [n]
2 (if (zero? n)
3 0
4 (+ n (recur (dec n)))) ; Error: `recur` is not in tail position
5 )
In this example, the recur call is not in the tail position because it is wrapped inside the + operation. To fix this, we need to refactor the code to ensure recur is the last operation:
Corrected Version:
1(defn sum [n]
2 (letfn [(sum-helper [acc n]
3 (if (zero? n)
4 acc
5 (recur (+ acc n) (dec n))))]
6 (sum-helper 0 n)))
7
8;; Usage
9(sum 5) ; => 15
Another limitation is that recur can only recur to the nearest enclosing function or loop. This means you cannot use recur to jump back to an outer function or loop.
Example of Incorrect Usage:
1(defn outer-function [n]
2 (defn inner-function [m]
3 (if (zero? m)
4 (recur (dec n)) ; Error: `recur` cannot jump to `outer-function`
5 (recur (dec m))))
6 (inner-function n))
In this example, recur is incorrectly used to try to jump back to outer-function. Instead, recur can only be used within inner-function.
Corrected Version:
To achieve similar functionality, you can use a loop construct:
1(defn outer-function [n]
2 (loop [n n]
3 (if (zero? n)
4 0
5 (recur (dec n)))))
6
7;; Usage
8(outer-function 5) ; => 0
recur RequirementsTo effectively use recur, we often need to refactor our code to ensure that recursive calls are in the tail position and that they recur to the nearest enclosing function or loop. Here are some strategies to achieve this:
Helper functions can encapsulate the recursive logic and ensure that recur is in the tail position.
Example:
1(defn fibonacci [n]
2 (letfn [(fib-helper [a b n]
3 (if (zero? n)
4 a
5 (recur b (+ a b) (dec n))))]
6 (fib-helper 0 1 n)))
7
8;; Usage
9(fibonacci 10) ; => 55
loop and recurThe loop construct can be used to create a local recursion point, allowing recur to be used effectively.
Example:
1(defn gcd [a b]
2 (loop [a a b b]
3 (if (zero? b)
4 a
5 (recur b (mod a b)))))
6
7;; Usage
8(gcd 48 18) ; => 6
recur with Java’s Iterative ConstructsIn Java, recursion is often avoided in favor of iterative constructs like loops due to the lack of tail-call optimization. Let’s compare how similar problems are solved in Java and Clojure.
Java Example: Factorial Calculation
1public class Factorial {
2 public static int factorial(int n) {
3 int result = 1;
4 for (int i = 1; i <= n; i++) {
5 result *= i;
6 }
7 return result;
8 }
9
10 public static void main(String[] args) {
11 System.out.println(factorial(5)); // Output: 120
12 }
13}
Clojure Example: Factorial Calculation
1(defn factorial [n]
2 (loop [acc 1 n n]
3 (if (zero? n)
4 acc
5 (recur (* acc n) (dec n)))))
6
7;; Usage
8(factorial 5) ; => 120
In Clojure, we use loop and recur to achieve the same result without growing the call stack, while in Java, we use a simple for loop.
recur LimitationsTo better understand the limitations of recur, let’s visualize the flow of a recursive function using a diagram.
graph TD;
A[Start] --> B{Check Base Case}
B -->|Yes| C[Return Result]
B -->|No| D[Perform Operation]
D --> E[Recur with New Arguments]
E --> B
Diagram Explanation:
recur.This diagram illustrates the flow of a recursive function using recur, highlighting the requirement for recur to be in the tail position.
To deepen your understanding, try modifying the following code examples:
recur to sum a list of numbers.recur to generate a sequence of Fibonacci numbers up to a given limit.recur:1(defn sum-of-squares [n]
2 (if (zero? n)
3 0
4 (+ (* n n) (sum-of-squares (dec n)))))
Implement a Recursive Function to Reverse a List Using recur.
Compare the Performance of a Recursive Function with and Without recur for Large Inputs.
recur Must Be in Tail Position: Ensure that recur is the last operation in a function or loop to avoid compilation errors.recur can only jump back to the nearest enclosing function or loop, not to outer scopes.recur Requirements: Use helper functions and loop constructs to refactor code and accommodate recur limitations.recur provides explicit stack-safe tail-position jumps, while ordinary Java recursion can lead to stack overflow.By understanding these limitations and strategies, you can effectively use recur in your Clojure programs, leveraging its power to write efficient and elegant recursive functions.
For more information on recursion and recur in Clojure, consider exploring the following resources:
recur Examples