├── .editorconfig ├── .gitignore ├── LICENSE ├── README.md ├── _config.yml ├── benchmark.md ├── pom.xml └── src ├── main └── java │ └── com │ └── github │ └── kag0 │ └── tail │ └── Tail.java └── test └── java └── other └── Test.java /.editorconfig: -------------------------------------------------------------------------------- 1 | [*] 2 | end_of_line = lf 3 | indent_style = tab 4 | indent_size = 2 5 | charset = utf-8 6 | 7 | [*.md] 8 | indent_style = space -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | pom.xml.tag 3 | pom.xml.releaseBackup 4 | pom.xml.versionsBackup 5 | pom.xml.next 6 | release.properties 7 | dependency-reduced-pom.xml 8 | buildNumber.properties 9 | .mvn/timing.properties 10 | # https://github.com/takari/maven-wrapper#usage-without-binary-jar 11 | .mvn/wrapper/maven-wrapper.jar 12 | .idea* 13 | *.iml -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # com.github.kag0.tail 2 | 3 | simple tail call optimization for Java 4 | 5 | enables infinitely deep [tail recursive calls](https://en.wikipedia.org/wiki/Tail_call) without throwing a `StackOverflowError` 6 | 7 | no transitive dependencies 8 | 9 | ## Install 10 | [![](https://jitpack.io/v/nrktkt/tail.svg)](https://jitpack.io/#nrktkt/tail) 11 | 12 | add the jitpack repository 13 | ```xml 14 | 15 | ... 16 | 17 | jitpack.io 18 | https://jitpack.io 19 | 20 | ... 21 | 22 | ``` 23 | add the dependency 24 | ```xml 25 | 26 | ... 27 | 28 | com.github.nrktkt 29 | tail 30 | Tag 31 | 32 | ... 33 | 34 | ``` 35 | 36 | ## Use 37 | 38 | ```java 39 | import com.github.kag0.tail.Tail; 40 | import static com.github.kag0.tail.Tail.*; 41 | 42 | Tail infiniteLoop(int i) { 43 | System.out.println("Loop " + i + ", stack still intact!"); 44 | return call(() -> infiniteLoop(i + 1)); 45 | } 46 | 47 | infiniteLoop(0).evaluate(); 48 | ``` 49 | 50 | ### Example: tail optimizing factorial computation 51 | 52 | #### un-optimized first version 53 | 54 | let's start with a simple recursive method to compute the `n`th factorial. 55 | this code will throw a `StackOverflowError` for large values of `n`. 56 | 57 | ```java 58 | long factorial(long n) { 59 | if(n == 1) return 1; 60 | else return n * factorial(n - 1); 61 | } 62 | ``` 63 | 64 | #### move the recursive call into the tail position 65 | 66 | the tail position is just another way of saying 67 | "the last thing you do before the `return`". 68 | 69 | ```java 70 | long factorial(long fact, long n) { 71 | if(n.equals(1)) return fact; 72 | return factorial(fact * n, n - 1); 73 | } 74 | ``` 75 | 76 | this may require a slight refactor, 77 | usually to add an additional parameter to accumulate progress. 78 | 79 | #### wrap the return type in `Tail` 80 | 81 | this will enforce that the recursive call is in the tail position. 82 | 83 | ```java 84 | Tail factorial(long fact, long n) 85 | ``` 86 | 87 | #### wrap base cases with `done` 88 | 89 | ```java 90 | if(n.equals(0)) return done(fact); 91 | ``` 92 | 93 | #### wrap recursive calls with `call` 94 | 95 | ```java 96 | return call(() -> factorial(fact * n, n - 1)); 97 | ``` 98 | 99 | #### profit 100 | 101 | call `.evaluate()` on the invocation of your method. 102 | 103 | ```java 104 | factorial(1, Long.MAX_VALUE).evaluate(); 105 | ``` 106 | 107 | recursive methods no longer blow the stack. 108 | note that if you skip the 'move the recursive call into the tail position' 109 | step, the code will not compile because the method is not tail recursive 110 | and therefore not stack safe. thanks to `Tail` that is covered by type safety. 111 | 112 | ### making safe recursive calls outside the tail position 113 | 114 | in addition to making tail recursion safe, 115 | we can also use trampolining to enable recursive methods 116 | that would otherwise be tricky to make tail recursive. 117 | 118 | to do this, just use `.flatMap` to chain two `call`s together. 119 | for example 120 | 121 | ```java 122 | Tail ackermann(int m, int n) { 123 | if(m == 0) 124 | return done(n + 1); 125 | if(m > 0 && n == 0) 126 | return call(() -> ackermann(m - 1, 1)); 127 | if(m > 0 && n > 0) 128 | return call(() -> ackermann(m, n - 1)).flatMap(nn -> ackermann(m - 1, nn)); 129 | throw new IllegalArgumentException(); 130 | } 131 | ``` 132 | 133 | ## [Benchmarks](benchmark.md) 134 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-minimal -------------------------------------------------------------------------------- /benchmark.md: -------------------------------------------------------------------------------- 1 | # Benchmarking 2 | 3 | in general I think benchmarking this library for comparison is a bit apples to oranges. 4 | the library enables deep recursive methods, and unless we're comparing to other 5 | deep recursion then we're not really comparing the same things. 6 | on this page we'll compare either 7 | a trampolined tail-recursive solution to a normal recursive solution for a shallow depth. 8 | this doesn't make a lot of sense because the latter is incapable of deep recursion, 9 | and we'd only ever care about using the former if the recursion was deep. 10 | or 11 | a recursive solution to an iterative solution. 12 | this doesn't make much sense because they're different implementations, 13 | and not all problems can easily be solved both ways. 14 | 15 | what does matter is understanding the impact we have on memory. 16 | not how much, but how it behaves. 17 | 18 | ## Memory Impact 19 | 20 | the take away here is that rather than using stack memory, 21 | young gen heap memory is used. 22 | as a consequence, other objects may be moved from young to old heap unnecessarily. 23 | 24 | note that this difference is only applicable to methods that use primitives. 25 | any method using objects or boxed primitives is going to generate the same 26 | amount of objects each loop. 27 | 28 | ## Performance 29 | 30 | Comparing safe and unsafe factorial computation with and without using `Tail`. 31 | Both implementations were otherwise identical (both tail recursive). 32 | `iter`- implementations are written with a `for` loop and no recursion. 33 | 34 | ``` 35 | # JMH version: 1.23 36 | # VM version: JDK 11.0.6, OpenJDK 64-Bit Server VM, 11.0.6+10 37 | # VM invoker: C:\Program Files\AdoptOpenJDK\jdk-11.0.6.10-hotspot\bin\java.exe 38 | # Warmup: 2 iterations, 10 s each 39 | # Measurement: 3 iterations, 10 s each 40 | # Timeout: 10 min per iteration 41 | # Threads: 2 threads, will synchronize iterations 42 | # Benchmark mode: Throughput, ops/time 43 | 44 | Benchmark Mode Cnt Score Error Units 45 | safe100 thrpt 3 281133.189 ± 7150.046 ops/s 46 | safe1000 thrpt 3 5556.739 ± 2087.674 ops/s 47 | safe10000 thrpt 3 53.123 ± 2.157 ops/s 48 | safe100000 thrpt 3 0.432 ± 0.087 ops/s 49 | unsafe100 thrpt 3 319579.637 ± 47343.027 ops/s 50 | unsafe1000 thrpt 3 5703.756 ± 2433.366 ops/s 51 | unsafe10000 thrpt 3 52.861 ± 0.823 ops/s 52 | unsafe100000 thrpt StackOverflowError 53 | iter1000 thrpt 5 7021.811 ± 71.309 ops/s 54 | iter10000 thrpt 5 61.583 ± 1.298 ops/s 55 | iter100000 thrpt 5 0.504 ± 0.020 ops/s 56 | ``` 57 | 58 | ### Implementations 59 | 60 | ```java 61 | static BigInteger factorial(BigInteger result, BigInteger n) { 62 | if(n.equals(BigInteger.ONE)) { 63 | return result; 64 | } 65 | return factorial(result.multiply(n), n.subtract(BigInteger.ONE)); 66 | } 67 | 68 | static Tail tailRecFactorial(BigInteger result, BigInteger n) { 69 | if(n.equals(BigInteger.ONE)) { 70 | return done(result); 71 | } 72 | return call(() -> tailRecFactorial(result.multiply(n), n.subtract(BigInteger.ONE))); 73 | } 74 | 75 | static BigInteger iterativeFactorial(BigInteger n) { 76 | BigInteger result = BigInteger.ONE; 77 | for(BigInteger i = BigInteger.ONE; i.compareTo(n) < 0; i = i.add(BigInteger.ONE)) { 78 | result = result.multiply(i); 79 | } 80 | return result; 81 | } 82 | ``` 83 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | com.github.kag0 8 | tail 9 | 0.2.0 10 | 11 | 12 | 1.8 13 | 1.8 14 | 15 | -------------------------------------------------------------------------------- /src/main/java/com/github/kag0/tail/Tail.java: -------------------------------------------------------------------------------- 1 | package com.github.kag0.tail; 2 | 3 | import java.util.function.Function; 4 | import java.util.function.Supplier; 5 | 6 | @FunctionalInterface 7 | public interface Tail { 8 | 9 | Tail next(); 10 | 11 | static Done done(A result) { 12 | return new Done<>(result); 13 | } 14 | 15 | static Tail call(Supplier> recursive) { 16 | return recursive::get; 17 | } 18 | 19 | default Tail flatMap(Function> fn) { 20 | return new FlatMap<>(this, fn); 21 | } 22 | 23 | static A evaluate(Tail call) { 24 | while(!(call instanceof Done)) { 25 | call = call.next(); 26 | } 27 | return ((Done) call).result; 28 | } 29 | 30 | default A evaluate() { 31 | return evaluate(this); 32 | } 33 | 34 | final class Done implements Tail { 35 | public final A result; 36 | 37 | public Done(A result) { 38 | this.result = result; 39 | } 40 | 41 | public Tail next() { 42 | return this; 43 | } 44 | 45 | public String toString() { 46 | return "Done{" + 47 | "result=" + result + 48 | '}'; 49 | } 50 | } 51 | 52 | final class FlatMap implements Tail { 53 | public final Tail tail; 54 | public final Function> fn; 55 | 56 | public FlatMap(Tail tail, Function> fn) { 57 | this.tail = tail; 58 | this.fn = fn; 59 | } 60 | 61 | public Tail next() { 62 | if(tail instanceof Done) { 63 | return fn.apply(((Done) tail).result); 64 | } 65 | if(tail instanceof FlatMap) { 66 | FlatMap fm = (FlatMap) tail; 67 | return fm.tail.flatMap(tail2 -> fm.fn.apply(tail2).flatMap(fn)); 68 | } 69 | return () -> tail.next().flatMap(fn); 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/test/java/other/Test.java: -------------------------------------------------------------------------------- 1 | package other; 2 | 3 | 4 | import com.github.kag0.tail.Tail; 5 | import static com.github.kag0.tail.Tail.*; 6 | 7 | import java.math.BigInteger; 8 | 9 | 10 | class Test { 11 | public static void main(String[] args) { 12 | try { 13 | fact(BigInteger.ONE, BigInteger.valueOf(99999)); 14 | throw new RuntimeException("JVM stack too big for this test"); 15 | } catch (StackOverflowError e) {} 16 | 17 | try { 18 | dangerousFact(BigInteger.ONE, BigInteger.valueOf(99999)); 19 | throw new RuntimeException("JVM stack too big for this test"); 20 | } catch (StackOverflowError e) {} 21 | 22 | try { 23 | unsafeAck(4, 1); 24 | throw new RuntimeException("JVM stack too big for this test"); 25 | } catch (StackOverflowError e) {} 26 | 27 | System.out.println(safeFact(BigInteger.ONE, BigInteger.valueOf(99999)).evaluate()); 28 | System.out.println(ack(4, 1).evaluate()); 29 | } 30 | 31 | static int unsafeAck(int m, int n) { 32 | if(m == 0) 33 | return n + 1; 34 | if(m > 0 && n == 0) 35 | return unsafeAck(m - 1, 1); 36 | if(m > 0 && n > 0) 37 | return unsafeAck(m - 1, unsafeAck(m, n- 1)); 38 | throw new IllegalArgumentException(); 39 | } 40 | 41 | static Tail ack(int m, int n) { 42 | if(m == 0) 43 | return done(n + 1); 44 | if(m > 0 && n == 0) 45 | return call(() -> ack(m - 1, 1)); 46 | if(m > 0 && n > 0) 47 | return call(() -> ack(m, n - 1).flatMap(nn -> ack(m - 1, nn))); 48 | throw new IllegalArgumentException(); 49 | } 50 | 51 | static Tail infiniteLoop(int i) { 52 | System.out.println("Loop " + i + ", stack still intact!"); 53 | 54 | return call(() -> infiniteLoop(i + 1)); 55 | } 56 | 57 | static BigInteger fact(BigInteger fact, BigInteger n) { 58 | if(n.equals(BigInteger.ZERO)) { 59 | return fact; 60 | } 61 | return fact(fact.multiply(n), n.subtract(BigInteger.ONE)); 62 | } 63 | 64 | static Tail dangerousFact(BigInteger fact, BigInteger n) { 65 | if(n.equals(BigInteger.ZERO)) { 66 | return done(fact); 67 | } 68 | return dangerousFact(fact.multiply(n), n.subtract(BigInteger.ONE)); 69 | } 70 | 71 | static Tail safeFact(BigInteger fact, BigInteger n) { 72 | if(n.equals(BigInteger.ZERO)) { 73 | return done(fact); 74 | } 75 | return call(() -> safeFact(fact.multiply(n), n.subtract(BigInteger.ONE))); 76 | } 77 | } --------------------------------------------------------------------------------