PurelyFunctional.tv Newsletter 361: Tip: trampoline your tail recursion

Issue 361 - January 20, 2020 · Archives · Subscribe

Clojure Tip 💡

trampoline your tail recursion

A few issues ago, I made the big distinction in Clojure between non-tail recursion and tail recursion. Non-tail-recursion uses a stackframe per recursion, so you risk blowing the stack. You can replace tail recursion with the recur form, which will optimize your recursion into a loop---and hence no stack explosion.

However, there are some things that look like tail calls but aren't really. For example, you can't do a recur inside of a try. And recur only works with self-recursion (a function calling itself) and not tail calls in general. Those add to the stack, too. Is there a way to avoid a stack explosion where we can't use recur?

Yes! It's called trampoline. Trampolining is a general technique of simulating a loop with return values. You run a function, it returns the next function to run, and the trampoline runs it. It keeps bouncing until you stop returning the next function to run. In the case of trampoline in Clojure, it keeps bouncing until you return a value that is not a function.

Let's take a look at a function that will continue to retry something in case of an error.

(defn retry-forever [f]
  (try
    (f)
    (catch Exception e
      (println "Got an error, retrying.")
      (recur f))))

This won't work. The recur looks like it's in tail position, but the try/catch needs to clean up before you return. You can't use recur inside of a try to call the enclosing function. We could switch it out to do regular recursion, but then this would eventually blow the stack.

(defn retry-forever [f]
  (try
    (f)
    (catch Exception e
      (println "Got an error, retrying.")
      (retry-forever f)))) ;; may blow the stack

Things seem dire, but they're not! A trampoline can help. Recall from above that a trampoline expects to be passed a function that is the next iteration of the loop. It will call that function. If it returns a function, it will call that, etc. Instant loop! All we have to do it make retry-forever return a function instead of recursing itself. In Clojure, this is a one-character change. See if you can spot the character.

(defn retry-forever [f]
  (try
    (f)
    (catch Exception e
      (println "Got an error, retrying.")
      #(retry-forever f)))) ;; return a function for the next
iteration

Notice that we are now returning a function, which tells the trampoline what the next iteration of the loop is. retry-forever is not technically recursive now. It is not calling itself. All branches (the success and the catch branches) return. That means the stack frame will be popped before retry-forever is called again.

To use this, we just have to call it like this:

(trampoline (retry-forever f))

That's great, but not terribly convenient. The caller shouldn't really care how this is implemented. Let's move the trampoline inside the function. We'll do the important work in a helper.

(defn retry-forever* [f]   ;; helper
  (try
    (f)
    (catch Exception e
      (println "Got an error, retrying.")
      #(retry-forever* f))))

(defn retry-forever [f]
  (trampoline (retry-forever* f)))

Now the caller doesn't even know it's a trampoline.

But there's another problem. What if f returns a function? trampoline is not smart enough to know that it is a special return value, not the next loop iteration. trampoline will call the returned function, like all the others.

The solution is to wrap it up in a non-function. Here's one solution:

(defn retry-forever* [f]   ;; helper
  (try
    {:value (f)}
    (catch Exception e
      (println "Got an error, retrying.")
      #(retry-forever* f))))

(defn retry-forever [f]
  (:value (trampoline (retry-forever* f))))

Now the helper is either returning a map or a function, regardless of the return value of f. We can unwrap it when trampoline returns from its bouncing. Wrap and unwrap.

That will work, but there's a built-in that is a better fit for indicating the end of a loop. It's called reduced. It's made for signaling to reduce that it can stop and return a value instead of continuing to the end of the sequence. We can use it here with its opposite, unreduced.

(defn retry-forever* [f]   ;; helper
  (try
    (reduced (f))
    (catch Exception e
      (println "Got an error, retrying.")
      #(retry-forever* f))))

(defn retry-forever [f]
  (unreduced (trampoline (retry-forever* f))))

You can use trampoline for optimizing mutual tail recursion as well. Mutual recursion is where a calls b and b calls a. They can't be optimized by the JVM (yet!), so they too can blow the stack. But trampolining can help. Mutual recursion comes up a lot in things like recursive descent parsers. That's a bit too much to go into now, but you can learn more about that parsing technique in this lesson.

One quick note about tradeoffs: trampolining is more expensive than tail call optimization. The function has to unroll the stack, the trampoline has to check the type, and then do another function call. Tail call optimization can be as quick as a jump instruction (GOTO). But on our platform, sometimes tail call optimizations are not possible.

Relevant courses 📚

If you like recursion, laziness, and reduce, check out these courses.

Book update 📖

Chapter 6 is out! Buy it now: Grokking Simplicity.

Chapter 6 is all about immutable disciplines you can apply to your code. The two disciplines are copy-on-read and copy-on-write. They let you have immutable data when your language doesn't provide it.

Chapter 7 is finally coming along. Design is a very intuitive, feeling-led endeavor. It is not 10 0% objective or quantifiable. I'm focused now on showing how the reader can develop their intuition along the lines of stratified design.

I'm not sure anyone has written as deeply as I am about it---I haven't found anything. There are articles written about it, but they are descriptive, not prescriptive. They characterize it, but don't tell you how to do it yourself. This will be an important chapter to differentiate the book. Finally, a book teaching FP design!

You can buy the book and use the coupon code TSSIMPLICITY for 50% off.

Podcast episode🎙

I've revamped the format of my podcast. If you like reading old computer science papers to really understand how we got here (and sometimes uncover old gems), this podcast is for you.

In the first episode of the season, we read a paper from 1987 by Abelson and Sussman, the authors of SICP, called * Lisp: A language for stratified design*. The paper asks the question: "What is Lisp good for?" Read the paper, then follow along on the podcast.

And speaking of podcasts, I was a guest on Functional Geekery and Defn.

State of Clojure Community Survey 📋

Please support the ongoing development of Clojure by filling out the annual survey. There are still a few days left.

This survey has been going on for years and it collects very important statistics on the Clojure community. I find the information valuable myself. I can only imagine how important it would be for the folks in Clojure's core team to know this stuff.

Please fill it out, regardless of how you use Clojure. It's all great information.

Cognit e ct analyzes the data and produces a summary. Here is the one from last year. They also release all of the data. Daniel Compton also analyzes the free-text responses and writes a summary. Here is his from last year.

Clojure Challenge 🤔

Last week's challenge

The challenge in Issue 360 was a function to find the coordinates of a particular value in a nested vector. You can see the submissions.

Peter Strömberg asks for comments on his implementation. You can leave comments on this or any gist in the gist itself. Please leave comments! We're all here to learn.

This week's challenge

Gapful numbers

Some numbers are gapful, some aren't. 100 is gapful because it has at least three digits and 100 is divisible by 10 ((str 1 0), the concatenation of the first and last digit). That's the definition of gapful: it has at least three digits and is divisible by the number formed by concatenating the first and last digits.

Create a function that takes a number and finds the closest gapful number. The function should return its argument if the argument itself is gapful. And if there are two gapful numbers equidistant to the argument, return the lower one.

Thanks to to this site for the challenge idea!

As usual, please reply to this email and let me know what you tried. I'll collect them up and share them in the next issue. If you don't want me to share your submission, let me know.

Rock on!
Eric Normand