Trampolining: a practical guide for awesome Java Developers
Trampolining is a nifty trick that every Java developer should know about. By representing a computation in one of 2 states (completed with result, or a reference to the reminder of the computation, something like the way a java.util.Supplier does) it is possible to implement algorithms recursively in Java without blowing the stack and to interleave the execution of functions without hard coding them together or even using threads.
In this article I hope to show you how trampolines can work for you, persuade you that understanding the basics is attainable for all Java developers, that the code verbosity impact is minimal and that the benefits can be significant.
Trampolines : Recursion without Stacks
A Trampoline is really a relatively straightforward concept. It is an interface with two concrete implementations, one of which simply represents the result and another that represents the next stage of the computation.
By breaking a computation down into individual steps we gain a lot of benefits.
One of which is the ability to recurse without blowing the stack. We can move the execution of any recursive method call outside of the method that makes the call and thus turn recursion into iteration.
Example Recursive algorithm : Fibonacci generation
Some algorithms can be very neatly defined recursively and the almost prototypical example of an algorithm that can be implemented elegantly recursively is the Fibonacci sequence. Making use of the cyclops-react Trampoline (javadoc link) control structure it can be defined quite succintly as follows
What is happening?
When get is called on the returned Trampoline, internally it will iterate calling ‘bounce’ on the returned Trampoline as long as the concrete instance returned is More, stopping once the returned instance is Done. Essential we convert looping via recursion into iteration, the key enabling mechanism is the fact that Trampoline.more is a lazy operation. Trampoline in cyclops-react extends java.util.Supplier. Calling Trampoline.more we are basically creating a Supplier that defers the actual recursive call, and having defered the call we can move it outside of the recursive loop. This means we can define algorithms recursively in Java but execute them iteratively. To delve deeper, while there are many ways to implement Trampolines in Java, for two particularly elegant examples I recommend looking at are Mario Fusco’s Presentation on Laziness and Trampolines (http://www.slideshare.net/mariofusco/lazine) and Daniel Bodart’s Totally Lazy’s Trampoline implementation (https://github.com/bodar/totallylazy/blob/master/src/com/googlecode/totallylazy/Trampoline.java)
The iterative equivalent is more verbose than it’s recursive cousin.
Trampolines : concurrency without threads
Another benefit is the ability to interleave the execution of two or more functions on the same thread. A technique we use internally in cyclops-react, to have data consumers switch to become data producers when no data is available inside our advanced parallel stream type LazyFutureStream.
Consumer to producer co-routine
Let’s show how Trampolines can be used to create co-routines, interleaved functions running on the same thread. In the example below we have a bog standard JDK Queue, populated with a single value. We want to read three values however, and we don’t want to fail on any read. If no data is available we would like our reader to handle that case and generate some.
The easiest way to interleave functions is simply to write them (hard code them) together. This is simple but not flexible, it ties us to a particular implementation. We can make use of Trampolines to interleave functions in a more generic way. In our example, we can pass in a trampoline that causes a data consumer to produce data if none is present. While leaving our code base flexible enough to handle other strategies in the future (perhaps the consumer should sleep for a period? or hog the current thread constantly checking for new data as it arrives?).
When the Queue is empty the noDataHandler Trampoline is executed & as a result new data is added to the queue for the consumer to consume.
cyclops introduces the concept of a FutureStream, this is essential a Stream of data with each data point embeded inside a Future. Clients do not manipulate the Futures directly themselves but rather define (advanced) Stream operations as normal and the library manages the pipeline of Futures and their parallel execution for you.
In the visualization of Future Streams below, we can see individual Future tasks managed by the Stream. Where a 1:1 relationship exists between Futures in one phase to the next (for example during a map operation) this process is pretty simple. The Stream can simply delegate the operation to the Future.
Were such a 1:1 relationship does not exist (for example during filter, flatMap, limit, debounce, grouped, sliding etc), the library has to step in to manage the transition.
I wrote a blog entry back in 2015 about how to ‘plumb’ Streams using async.Queue’s (link : https://medium.com/@johnmcclean/plumbing-java-8-streams-with-queues-topics-and-signals-d9a71eafbbcc#.lf0v5ddxx). And this is exactly what we do. On each side of the Queue that handles the more advanced operators we have two separate Streams, and we make use of a very similar technique to that described above to allow consuming Streams both to trigger the producing Streams in the first place, but also to ensure that producing Streams have the correct maximum number of active Future tasks working to produce data.
Building Trampolines into the APIs
The Eval type in cyclops-react is a monadic container for Lazy values. It is a Supplier on steroids, and it’s implementation was heavily influenced by Eval from the Scala library Cats (see http://eed3si9n.com/herding-cats/Eval.html).
Evals represent one of three states.
- Now : An eagerly evaluated value
- Later : A value that will be evaluated exactly once in the future if triggered
- Always : A value that will be evaluated every time it is accessed in the future.
The primary use case for Eval is to avoid doing unnecessary or expensive work (and to avoid doing it multiple times if needed). For example we can use Eval to encapsulate some expensive operation, or series of operations, and only execute them if absolutely necessary :-
Tail recursion for map / flatMap operators
Like Cat’s Eval, cyclops-react version has tail call optimization for map and flatMap operators, as it implements it’s own internal Trampoline for executing these operators. Although less elegant, we can rewrite the fibonacci algorithm using Eval :-
The Maybe type in cyclops-react is a totally lazy Haskell stye ‘Option’ type. That is unlike Java’s Optional or Scala’s Option type all operations are lazy. In fact Maybe in cyclops-react is built using Eval. This makes Maybe an excellent return type for expensive operations that may or may not return a value.
Maybe is a lazy Option(al) type
Tail call optimization for map / flatMap operators
Because Maybe is built using Eval, we also inherit it’s tail-call optimization during map & flatMap operations.
Match on structures
cyclops also has a pattern matching API. Structural pattern matching in cyclops is implemented via product (Tuples) and sum (Either / Xor) types. Like an Object a Tuple consists of multiple typed fields. Each field must be of the defined type. But unlike a heterogeneous Java Objects, which are defined by their clasess, Tuples have a predefined structure that we can take advantage of at compile time. Sum types (such as Eithers or Xor’s) also have a predefined structure this time determining a number of possible types, of which a field must be one.
Sum and product types allow us to understand at compile time what types are acceptable for any given field. In cyclops we convert Objects to sum and product combinations to match on their structure at compile time.
The simplest structure (except for Void) is one of a single value of a known type. E.g. we can pattern match on a single number
In the example above we have defined intermediate variables for each stage, to make what is going on clearer. But also to highlight some key points
- We are matching on structures (MTuple1 tells us we know there is one value of the defined type Integer)
- Pattern matching is lazy (the return type is Eval)
- Pattern matching is tail-recursive (the return type is Eval)
Tail call optimization for map / flatMap operators
Because cyclops pattern matching makes heavy use of Eval, like Eval itself and Maybe map (and other functor operators) as well as flatMap (and other monadic operators) are also tail-call optimized.
For example we could re-implement the even / odd example from Herding Cats (http://eed3si9n.com/herding-cats/Eval.html) using cyclops pattern matching.
Trampolining in a Stream (and other functors!)
Some algorithms can be expressed more elegantly and less verbosely in recursive form, but because Java lacks native Tail Call Optimization often we need to add a little more verbosity to our code in order to write stackless recursive implementations. Throughout the api in cyclops-react we try to make using Trampolines as painless as possible. The functor interface, implemented by a swathe of cyclops control structures including ReactiveSeq (a powerful sequential extended Stream type), LazyFutureStream (a powerful parallel extended Stream type), extended collections, Xor, Maybe, Try and more, includes a trampolining transformation — an excellent choice if you would like to implement your transformation function recursively or interleave multiple co-routines within.
E.g to generate a Fibonacci sequence repeatedly from data in an input Queue we could write something like this :
In my view, while the code style overhead of using Trampolines to implement recursion in Java 8 is pretty minimal, I wouldn’t recommend that engineers start using them everywhere. There are many cases where a standard iterative implementation will be much clearer and easier to understand. Occasionally though, most of us will stumble across algorithms that are just so much neater when written recursively. Trampolines allow us to implement these algorithms in Java in a stack-safe way. For me though the biggest benefit of adopting this technique, is for those very hard to solve problems where code needs to be interleaved and we do not want to spawn extra threads. So long as we have solid reasons for doing so, using trampolines to implement co-routines in Java can make very tough problems a *lot* simpler.