Mechanically Deriving Binary Tree Iterators with Continuation Defunctionalization
- A twenty-two minute read
- 2 comments
- 2 π£οΈ 1 π
Binary tree is the simplest of tree data structures. It is a tree in which each node has at most two children. A tree traversal is a process of visiting each node in the tree, exactly once. There are multiple ways of traversing a binary tree in depth-first fashion with each traversal resulting in a different enumeration of the tree elements. These tree traversals are defined as simple recursive functions. But what if we want to write Java-style iterators for them? Is there a way to mechanically derive these iterators from the traversal functions? Letβs find out.
Traversals and Iterations
This is a sample binary tree:
βββββ
β D β
βββ¬ββ
ββββββββ΄ββββββββ
βββ΄ββ βββ΄ββ
β B β β F β
βββ¬ββ βββ¬ββ
βββββ΄ββββ βββββ΄ββββ
βββ΄ββ βββ΄ββ βββ΄ββ βββ΄ββ
β A β β C β β E β β G β
βββββ βββββ βββββ βββββ
Different traversals of this tree will yield different sequences of elements:
Traversal | Output |
---|---|
in-order | ABCDEFG |
pre-order | DBACFEG |
post-order | ACBEGFD |
The code for the recursive in-order traversal (traverse left child, then self, then right child) is very simple. Assuming a binary tree is represented with a Tree
class like:
class Tree<T> {
<T> left;
Tree;
T content<T> right;
Tree}
this code prints all elements of a tree in in-order:
static <T> void printRecursive(Tree<T> tree) {
if (tree != null) {
printRecursive(tree.left);
System.out.print(tree.content);
printRecursive(tree.right);
}
}
But the code for an iterator which iterates over a tree in in-order is much more complicated:
class InOrderIterator<T> implements Iterator<T> {
private Tree<T> tree;
private Stack<Tree<T>> stack = new Stack<>();
InOrderIterator(Tree<T> tree) { this.tree = tree; }
@Override
public boolean hasNext() {
return tree != null || !stack.isEmpty();
}
@Override
public T next() {
while (hasNext()) {
if (tree != null) {
.push(tree);
stack= tree.left;
tree } else {
if (!stack.isEmpty()) {
<T> t = stack.pop();
Tree= t.content;
T content = t.right;
tree return content;
}
}
}
throw new NoSuchElementException();
}
}
class Main {
public static void main(String[] args) {
<String> tree = new Tree<>(...);
Tree<String> inOrderIterator =
InOrderIteratornew InOrderIterator<>(tree);
while (inOrderIterator.hasNext()) {
System.out.print(inOrderIterator.next());
}
}
}
The iterator code uses a Stack
to simulate the program stack of the recursive traversal. It takes some thinking about the tree structure and the program flow to write this code and it is easy to get it wrong. Pre-order and Post-order iterators are even more complicated1. Is there a way to mechanically derive the iterators starting from the recursive traversals by following some rules? Indeed there is! Keep reading.
Binary Tree
Letβs start with some setup code:
import java.util.Iterator;
import java.util.NoSuchElementException;
import java.util.Stack;
import java.util.concurrent.ThreadLocalRandom;
import java.util.function.Consumer;
import java.util.function.Supplier;
class Utils {
static <T> void printContent(T t) {
System.out.printf(t + " ");
}
static String generateRandomAlphaString(int maxLength) {
= ThreadLocalRandom.current();
ThreadLocalRandom random int targetLength = random.nextInt(maxLength) + 1;
StringBuilder sb = new StringBuilder(targetLength);
for (int i = 0; i < targetLength; i++) {
.append((char) random.nextInt('a', 'z' + 1));
sb}
return sb.toString();
}
static StringBuilder makeGuidelines(int times) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < times; i++) {
.append("β ");
sb}
return sb.append("β ");
}
}
With these imports and utility functions out of our way, letβs first define a Binary Tree:
class Tree<T> {
<T> left;
Tree;
T content<T> right;
Tree
Tree(Tree<T> left, T content, Tree<T> right) {
this.left = left;
this.content = content;
this.right = right;
}
public static <T> Tree<T> generate(
int maxDepth, Supplier<T> gen, double nullProbability) {
if (nullProbability < 0 || nullProbability > 1) {
throw new IllegalArgumentException(
"nullProbability must be between 0 and 1");
}
if (maxDepth == 0) {
return null;
}
double rand = ThreadLocalRandom.current().nextDouble();
if (rand < nullProbability) {
return null;
}
<T> left = generate(maxDepth - 1, gen, nullProbability);
Tree<T> right = generate(maxDepth - 1, gen, nullProbability);
Treereturn new Tree<>(left, gen.get(), right);
}
@Override
public String toString() {
return toStringLayout(0).toString();
}
private StringBuilder toStringLayout(int level) {
StringBuilder sb = new StringBuilder()
.append(Utils.makeGuidelines(level))
.append(content)
.append("\n");
if (this.left == null && this.right == null) {
return sb;
}
StringBuilder nullChildGuidelines =
.makeGuidelines(level + 1);
Utilsif (this.left != null) {
.append(this.left.toStringLayout(level + 1));
sb} else {
.append(nullChildGuidelines).append("<NULL>\n");
sb}
if (this.right != null) {
.append(this.right.toStringLayout(level + 1));
sb} else {
.append(nullChildGuidelines).append("<NULL>\n");
sb}
return sb;
}
}
Each node in a binary tree has some content and two nullable child nodes. We have a constructor to create the tree. The generate
function generates an arbitrary binary tree of a given maximum depth by using a random content generator. We use this function to generate trees to test our traversals and iterators.
We also implement the toString
method for the tree to create a string representation of the tree to help us in debugging. A sample run:
<String> tree = Tree.generate(4, () ->
Tree.generateRandomAlphaString(2), 0.1);
UtilsSystem.out.println(tree);
Output:
β r
β β j
β β β x
β β β β e
β β β β m
β β β vz
β β β β g
β β β β <NULL>
β β l
β β β b
β β β β qc
β β β β g
β β β rp
β β β β d
β β β β o
Pictorially:
Weβll use this sample tree to test our code so refer back to this tree to check the correctness of the code.
Recursive Traversal
Credits first: this blog post and the code in it is inspired by The Best Refactoring Youβve Never Heard Of article (and talk) by James Koppel. In the talk, James shows how to transform a recursive in-order traversal into an iterative one. For this post, Iβve chosen to implement pre-order and post-order iterators.
Letβs start with the recursive pre-order traversal which prints the content of every node in the tree in pre-order: each nodeβs content is printed before its childrenβs.
static <T> void printRecursive(Tree<T> tree) {
if (tree != null) {
.printContent(tree.content);
UtilsprintRecursive(tree.left);
printRecursive(tree.right);
}
}
printRecursive(tree);
// r j x e m vz g l b qc g rp d o
Short and sweet, simple and easy to understand. If the tree is not null, we print its content, then we recursively print the left and right child trees.
First thing to do is to extract out the print action into a function argument so that we can do different kinds of actions on the nodes instead of just printing them:
static <T> void iterateRecursive(
<T> tree, Consumer<T> action) {
Treeif (tree != null) {
.accept(tree.content);
actioniterateRecursive(tree.left, action);
iterateRecursive(tree.right, action);
}
}
We use Consumer
for the type of the action. Since Consumer
is a functional interface, we can pass lambdas in its place. We can call it like this:
iterateRecursive(tree, Utils::printContent);
// r j x e m vz g l b qc g rp d o
This transformation was easy to grok. The next one requires a little head-tilting. We convert the simple recursion into a Continuation-passing style (CPS) recursion:
static <T> void iterateCPS(
<T> tree, Consumer<T> action, Runnable cont) {
Treeif (tree != null) {
.accept(tree.content);
actioniterateCPS(tree.left, action, () ->
iterateCPS(tree.right, action, cont));
} else {
.run();
cont}
}
So what are Continuations?
Continuations
In the usual direct imperative programming style, we write one statement after another, as a sequence of steps to execute. There is another way of thinking about it: after returning from executing one statement, the rest of the programβwhich can be thought of as a big statement itselfβis run. In Continuation-passing style, this way is made explicit: each statement takes the rest of the program which comes after it as an argument, which it invokes explicitly. For example, if we have a program written in direct style like this:
static int start(String s) {
int a = getSomething(s);
doAnotherThing(a);
return a;
}
It can be converted into an equivalent CPS style program like this:
static void startCPS(String s, Callable<Integer> cont) {
getSomethingCPS(s, (a) -> {
doAnotherThingCPS(a, () -> {
.call(a);
cont});
});
}
static void getSomethingCPS(String s, Callable<Integer> cont) {
.call(getSomething(s));
cont}
static void doAnotherThingCPS(int a, Runnable cont) {
doAnotherThing(a);
.run();
cont}
We see how each function call takes the rest of the program after it as a lambda and calls it explicitly to further the flow of the program. Instead of returning an int
value, the startCPS
function now takes a lambda as an additional Callable
argument which it calls with the int
value at the end of all the processing. These lambdas are known as Continuations because they continue the flow of the programs, and hence this style of writing programs is called the Continuation-passing style.
Comparing the direct and CPS recursive functions, we can now understand the transformation:
static <T> void iterateRecursive(
<T> tree, Consumer<T> action) {
Treeif (tree != null) {
.accept(tree.content);
actioniterateRecursive(tree.left, action);
iterateRecursive(tree.right, action);
}
}
static <T> void iterateCPS(
<T> tree, Consumer<T> action, Runnable cont) {
Treeif (tree != null) {
.accept(tree.content);
actioniterateCPS(tree.left, action, () ->
iterateCPS(tree.right, action, cont));
} else {
.run();
cont}
}
We first print the treeβs content just like before. But instead of calling the function itself recursively twice for each child node, we call it only once for the left child node and pass a continuation lambda as the last parameter, which when called, calls the iterateCPS
function for the right child node with the current continuation. This chain of continuations is called when the recursion bottoms out at the leftmost leaf node in line 17. Letβs run it:
iterateCPS(tree, Utils::printContent, () -> { return; });
// r j x e m vz g l b qc g rp d o
We pass an empty lambda to start with which will be the last continuation to be called.
Readers are suggested to take a while to grok this transformation because this is a crucial step. Hereβs how the program call stack looks like for this simple tree:
βββββ
β B β
βββ¬ββ
βββββ΄ββββ
βββ΄ββ βββ΄ββ
β A β β C β
βββββ βββββ
iterateCPS("B", ac, return); // prints "B"
iterateCPS("A", ac, () ->
iterateCPS("C", ac, return)); // prints "A"
iterateCPS(null, ac, () ->
iterateCPS(null, ac, () -> iterateCPS("C", ac, return)));
iterateCPS(null, ac, () -> iterateCPS("C", ac, return));
iterateCPS("C", ac, return); // prints "C"
iterateCPS(null, ac, () -> iterateCPS(null, ac, return));
iterateCPS(null, ac, return);
return;
Readers should try to imagine (or work out) the program call stack when it runs on the sample tree and see how continuations are layered over continuations.
Defunctionalization
Defunctionalization is replacing functions with data. In this context, it means replacing the continuation lambdas with objects. The reason for doing Defunctionalization will become clear as we proceed.2
For defunctionalizing the continuations, we need to find out all possible cases of continuations we have:
static <T> void iterateCPS(
<T> tree, Consumer<T> action, Runnable cont) {
Treeif (tree != null) {
.accept(tree.content);
actioniterateCPS(tree.left, action, () ->
iterateCPS(tree.right, action, cont));
} else {
.run();
cont}
}
iterateCPS(tree, Utils::printContent, () -> { return; });
Here, we have two cases of continuations which are highlighted above: first which recursively calls iterateCPS
and second which is an empty lambda which terminates the recursion. To capture these two cases with an object, we use this class:
class Cont<T> {
final Tree<T> tree;
final Cont<T> next;
Cont(Tree<T> tree, Cont<T> next) {
this.tree = tree;
this.next = next;
}
}
If the tree
field is set, itβs the first case, otherwise itβs the second case. We also need the cont
field to capture the current continuation which the first lambda captures in its third call argument. Now we replace all the lambdas with Cont
objects and the lambda invocation with the two case of continuations:
static <T> void iterateDefCPS(
<T> tree, Consumer<T> action, Cont<T> cont) {
Treeif (tree != null) {
.accept(tree.content);
actioniterateDefCPS(tree.left, action, new Cont<>(tree.right, cont));
} else {
if (cont != null) {
iterateDefCPS(cont.tree, action, cont.next);
} else {
return;
}
}
}
Corresponding parts of the code have been highlighted to clearly see the transformation. We can run it and see if it works:
iterateDefCPS(tree, Utils::printContent, null)
// r j x e m vz g l b qc g rp d o
It works! To visualize it better, letβs look at the program call stack again for the simple tree:
βββββ
β B β
βββ¬ββ
βββββ΄ββββ
βββ΄ββ βββ΄ββ
β A β β C β
βββββ βββββ
iterateDefCPS("B", ac, null); // prints "B"
iterateDefCPS("A", ac, Cont("C", null)); // prints "A"
iterateDefCPS(null, ac, Cont(null, Cont("C", null));
iterateDefCPS(null, ac, Cont("C", null));
iterateDefCPS("C", ac, null) // prints "C";
iterateDefCPS(null, ac, Cont(null, null));
iterateDefCPS(null, ac, null);
return
Again, readers are encouraged to spend some time thinking about and playing with this function to convince themselves that it works.
Path forward is pretty easy now.
Iteration
Notice how in the iterateDefCPS
function, all the recursive calls are at tail call positions. That is, the last thing done in the iterateDefCPS
function is to invoke itself recursively. Thatβs what we achieved with Defunctionalization. Now we can do tail call optimization and replace the recursive calls with iteration:
static <T> void iterate(
<T> tree, Consumer<T> action, Cont<T> cont) {
Treewhile (true) {
if (tree != null) {
.accept(tree.content);
action= new Cont<>(tree.right, cont);
cont = tree.left;
tree } else {
if (cont != null) {
= cont.tree;
tree = cont.next;
cont } else {
return;
}
}
}
}
We just wrap the whole function body in a infinite while
loop and instead of calling the function recursively, at the tail call points, we replace the function parameter variables with their new values. Rest of the code remains unchanged.
Upon running, it returns correct result:
iterate(tree, Utils::printContent, null);
// r j x e m vz g l b qc g rp d o
On to the final step, writing the Iterator.
Pre-order Iterator
To write the iterator, we need to realize that the Cont
class is nothing but a Stack. Creating a new Cont
object by passing it the current one is like pushing onto a stack. Similarly, writing cont = cont.next
is like popping a stack.
With this realization, we simply hoist the parameters of the iterateDefCPS
function into instance fields and replace Cont
with a stack to transform it to an Iterator:
class PreOrderIterator<T> implements Iterator<T> {
private Tree<T> tree;
private Stack<Tree<T>> stack = new Stack<>();
PreOrderIterator(Tree<T> tree) { this.tree = tree; }
@Override
public boolean hasNext() {
return tree != null || !stack.isEmpty();
}
@Override
public T next() {
while (hasNext()) {
if (tree != null) {
= tree.content;
T content if (tree.right != null) {
.push(tree.right);
stack}
= tree.left;
tree return content;
} else {
if (!stack.isEmpty()) {
= stack.pop();
tree }
}
}
throw new NoSuchElementException();
}
}
iterateDefCPS
returns when both tree
and cont
are null
. So that is our iteration termination condition and it captured as such in the hasNext
method of the iterator. next
method is pretty much a copy of the iterateDefCPS
function with Cont
replaced by a Stack
and an additional check to make sure to not push null elements onto the stack. We exercise the iterator like this:
<String> preOrderIterator =
PreOrderIteratornew PreOrderIterator<>(tree);
while (preOrderIterator.hasNext()) {
.printContent(preOrderIterator.next());
Utils}
// r j x e m vz g l b qc g rp d o
It prints the elements in correct order. This completes our mechanical derivation of the pre-order iterator from recursive traversal code.
Post-order Iterator
Post-order iterator derivation turns out to be a bit more complicated that the pre-order one. Nevertheless, letβs get started.
static <T> void printRecursive(Tree<T> tree) {
if (tree != null) {
printRecursive(tree.left);
printRecursive(tree.right);
.printContent(tree.content);
Utils}
}
static <T> void iterateRecursive(
<T> tree, Consumer<T> action) {
Treeif (tree != null) {
iterateRecursive(tree.left, action);
iterateRecursive(tree.right, action);
.accept(tree.content);
action}
}
static <T> void iterateCPS(
<T> tree, Consumer<T> action, Runnable cont) {
Treeif (tree != null) {
iterateCPS(tree.left, action, () -> {
iterateCPS(tree.right, action, () -> {
.accept(tree.content);
action.run();
cont}}));
} else {
.run();
cont}
}
We start with the recursive traversal function which prints the content of the node after recursively traversing the left and right child trees. We convert it to take an action instead of printing directly. Then we convert that form to a recursive CPS form.
The iterateCPS
function this time has two lambda continuations. The first continuation calls iterateCPS
recursively for the right child and the second one calls the action and invokes the current continuation. Letβs call these functions for our sample tree:
printRecursive(tree);
// e m x g vz j qc g b d o rp l r
iterateRecursive(tree, Utils::printContent);
// e m x g vz j qc g b d o rp l r
iterateCPS(tree, Utils::printContent, () -> { return; });
// e m x g vz j qc g b d o rp l r
It works as expected but letβs walk through the program call stack for our simple tree for iterateCPS
to understand it better:
βββββ
β B β
βββ¬ββ
βββββ΄ββββ
βββ΄ββ βββ΄ββ
β A β β C β
βββββ βββββ
iterateCPS("B", ac, return);
iterateCPS("A", ac, () -> iterateCPS("C", ac, () -> {
ac("B"); return
}));
iterateCPS(null, ac, () -> iterateCPS(null, ac, () -> {
ac("A"); iterateCPS("C", ac, () -> { ac("B"); return })
}));
iterateCPS(null, ac, () -> {
ac("A"); iterateCPS("C", ac, () -> {ac("B"); return })
});
ac("A"); // prints "A"
iterateCPS("C", ac, () -> { ac("B"); return });
iterateCPS(null, ac, () -> iterateCPS(null, ac, () -> {
ac("C"); ac("B"); return
}));
iterateCPS(null, ac, () -> { ac("C"); ac("B"); return });
ac("C"); // prints "C"
ac("B"); // print "B"
return;
I hope the call stack makes it clear how iterateCPS
works. Now we have to defunctionalize the continuations.
static <T> void iterateCPS(
<T> tree, Consumer<T> action, Runnable cont) {
Treeif (tree != null) {
iterateCPS(tree.left, action, () -> {
iterateCPS(tree.right, action, () -> {
action.accept(tree.content);
cont.run();
});
});
} else {
.run();
cont}
}
static <T> void iterateCPS(
<T> tree, Consumer<T> action, Runnable cont) {
Treeif (tree != null) {
iterateCPS(tree.left, action, () -> {
iterateCPS(tree.right, action, () -> {
action.accept(tree.content);
cont.run();
});
});
} else {
.run();
cont}
}
iterateCPS(tree, Utils::printContent, () -> { return; });
Unlike pre-order, in post-order iterateCPS
function, we have three different cases of continuation as highlighted above. The first and second cases recursively call iterateCPS
but the third one doesnβt. The first one however works with the left child node and the second one with the right child node. That is the distinction between them.
To defunctiontionalize these three cases, we create a different Cont
class:
class Cont<T> {
final Tree<T> tree;
final boolean isLeft;
final Cont<T> next;
Cont(Tree<T> tree, boolean isLeft, Cont<T> next) {
this.tree = tree;
this.next = next;
this.isLeft = isLeft;
}
}
We have added a new boolean field isLeft
to differentiate between the first and second cases. Now we replace the lambdas with Cont
objects:
static <T> void iterateDefCPS(
<T> tree, Consumer<T> action, Cont<T> cont) {
Treeif (tree != null) {
<T> rCont = new Cont<>(tree, false, cont);
Cont<T> lCont = new Cont<>(tree.right, true, rCont);
ContiterateDefCPS(tree.left, action, lCont);
} else {
if (cont != null) {
if (cont.isLeft) {
iterateDefCPS(cont.tree, action, cont.next);
} else {
action.accept(cont.tree.content);
iterateDefCPS(null, action, cont.next);
}
} else {
return;
}
}
}
We create a right continuation object and then a left continuation object with the right one wrapped inside it. This nesting corresponds to the nested lambdas of the CPS form. In the invocation section, we can see the corresponding highlighted sections in the same order as before. Readers are suggested to mull over this transformation to convince themselves that itβs correct. Letβs run it now:
iterateDefCPS(tree, Utils::printContent, null);
// e m x g vz j qc g b d o rp l r
Letβs walk through the call stack of iterateDefCPS
for the simple tree:
βββββ
β B β
βββ¬ββ
βββββ΄ββββ
βββ΄ββ βββ΄ββ
β A β β C β
βββββ βββββ
iterateDefCPS("B", ac, null);
iterateDefCPS("A", ac, Cont("C", true, Cont("B", false, null)));
iterateDefCPS(null, ac,
Cont(null, true,
Cont("A", false, Cont("C", true, Cont("B", false, null)))));
iterateDefCPS(null, ac,
Cont("A", false, Cont("C", true, Cont("B", false, null))));
ac("A"); // prints "A"
iterateDefCPS(null, ac, Cont("C", true, Cont("B", false, null)));
iterateDefCPS("C", ac, Cont("B", false, null));
iterateDefCPS(null, ac,
Cont(null, true, Cont("C", false, Cont("B", false, null))));
iterateDefCPS(null, ac, Cont("C", false, Cont("B", false, null)));
ac("C"); // prints "C"
iterateDefCPS(null, ac, Cont("B", false, null));
ac("B"); // prints "B"
iterateDefCPS(null, ac, null);
return;
Turning iterateDefCPS
into an iteration is straightforward now:
static <T> void iterate(
<T> tree, Consumer<T> action, Cont<T> cont) {
Treewhile (true) {
if (tree != null) {
= new Cont<>(tree, false, cont);
cont = new Cont<>(tree.right, true, cont);
cont = tree.left;
tree } else {
if (cont != null) {
if (cont.isLeft) {
= cont.tree;
tree } else {
.accept(cont.tree.content);
action= null;
tree }
= cont.next;
cont } else {
return;
}
}
}
}
All the recursive calls to iterateDefCPS
are at tail call position similar to pre-order case. As before, we wrap the function body in an infinite while
loop and replace recursive calls with assignment to the function parameter variables.
This works too:
iterate(tree, Utils::printContent, null);
// e m x g vz j qc g b d o rp l r
Writing the post-order iterator is a bit trickier than the pre-order case. Since the Cont
class now has a third field, we canβt transform it directly into a Stack
of Tree
s. We need to create another class β called TreeTup
here β to capture the tree and the isLeft
field value. Then we can create a stack of TreeTup
s. Rest of the transformation is pretty mechanical:
class PostOrderIterator<T> implements Iterator<T> {
private static class TreeTup<T> {
final Tree<T> tree;
final boolean isLeft;
public TreeTup(Tree<T> tree, boolean isLeft) {
this.tree = tree;
this.isLeft = isLeft;
}
}
private Tree<T> tree;
private Stack<TreeTup<T>> stack = new Stack<>();
PostOrderIterator(Tree<T> tree) { this.tree = tree; }
@Override
public boolean hasNext() {
return tree != null || !stack.isEmpty();
}
@Override
public T next() {
while (hasNext()) {
if (tree != null) {
.push(new TreeTup<>(tree, false));
stackif (tree.right != null) {
.push(new TreeTup<>(tree.right, true));
stack}
= tree.left;
tree } else {
if (!stack.isEmpty()) {
<T> tup = stack.pop();
TreeTupif (tup.isLeft) {
= tup.tree;
tree } else {
= tup.tree.content;
T content = null;
tree return content;
}
}
}
}
throw new NoSuchElementException();
}
}
We can compare the iterate
function with the next
method above and we see that itβs pretty much the same code except for an additional check to not push null values onto the stack.
Now for the final run:
<String> postOrderIterator =
PostOrderIteratornew PostOrderIterator<>(tree);
while (postOrderIterator.hasNext()) {
.printContent(postOrderIterator.next());
Utils}
// e m x g vz j qc g b d o rp l r
Conclusion
We have learned how to mechanically write pre-order and post-order iterators for binary trees. We started with simple recursive traversals and through a series of steps, we transformed them into Java-style iterators. Continuation Defunctionalization can be used to transform any recursion into an iteration. I hope it will come handy for you some day. The complete code with including the in-order iterator can be see here.
These traversals can be generalized to take a function which they call with the content of each node. Such traversals are examples of Internal Iterators. Java-style iterators on the other hand are examples of External Iterators.β©οΈ
Learn more about Defunctionalization in the Defunctionalization at Work paper by Olivier Danvy and Lasse R. Nielsen.β©οΈ
Got suggestions, corrections, or thoughts? Post a comment!
2 comments
jkff
notfancy