Here is the version that works on your linear-tree example. This is a direct transformation of your implementation with two changes: it uses a continuation style and a trampoline.
(defn tree->newick ([tree] (trampoline tree->newick tree identity)) ([tree cont] (let [{:keys [id children to-parent]} tree dist (double to-parent)] ; to-parent may be a rational (if children (fn [] (tree->newick (first children) (fn [s1] (fn [] (tree->newick (second children) (fn [s2] (cont (str "(" s1 "," s2 "):" dist)))))))) (cont (str (name id) ":" dist))))))
Edit : Added template matching, which allows you to simply call a function.
Change 2 . I noticed that I was mistaken. The problem is that I really accepted the fact that Clojure does not optimize tail calls only partially into account.
The main idea of ββmy solution is to convert to a continuation transfer style, so recursive calls can be moved to the tail position (i.e., instead of returning their result, recursive calls pass it on as an argument).
Then I manually optimized recursive calls, forcing them to use a trampoline. I forgot to consider that continuation calls, which are not recursive calls, but also in the tail position, also need to be optimized, since tail calls can be a very long chain of closures, so when the function finally evaluates them, it becomes a long chain of calls.
This problem did not materialize with the linear-tree test data, because the continuation for the first child returns to the trampoline to handle the recursive call for the second child. But if the linear-tree changed so that it uses the second child of each node to build a linear tree instead of the first child, this again causes a stack overflow.
Thus, appeals of follow-ups should also return to the springboard. (Actually, a call in the base case does not happen without children, because it will happen no more than once before returning to the springboard, and then this will be true for the second recursive call.) So, here is an implementation that takes this into account and should use only constant stack space on all inputs:
(defn tree->newick ([tree] (trampoline tree->newick tree identity)) ([tree cont] (let [{:keys [id children to-parent]} tree dist (double to-parent)] ; to-parent may be a rational (if children (fn [] (tree->newick (first children) (fn [s1] (tree->newick (second children) (fn [s2]