Como mencionas, la única forma de implementar esto utilizando la recursividad final es cambiar a usar una pila explícita. Un enfoque posible es convertir la estructura de árbol en una estructura de pila que es esencialmente una representación de anotación polaca inversa del árbol (utilizando un bucle y una pila intermedia para lograr esto). Luego usarías otro ciclo para atravesar la pila y construir tu resultado.
Aquí hay un programa de ejemplo que escribí para lograr esto, usando el código de Java en postorder using tail recursion como inspiración.
(def op-map {'+ +, '- -, '* *, '/ /})
;; Convert the tree to a linear, postfix notation stack
(defn build-traversal [tree]
(loop [stack [tree] traversal []]
(if (empty? stack)
traversal
(let [e (peek stack)
s (pop stack)]
(if (seq? e)
(recur (into s (rest e))
(conj traversal {:op (first e) :count (count (rest e))}))
(recur s (conj traversal {:arg e})))))))
;; Pop the last n items off the stack, returning a vector with the remaining
;; stack and a list of the last n items in the order they were added to
;; the stack
(defn pop-n [stack n]
(loop [i n s stack t '()]
(if (= i 0)
[s t]
(recur (dec i) (pop s) (conj t (peek s))))))
;; Evaluate the operations in a depth-first manner, using a temporary stack
;; to hold intermediate results.
(defn eval-traversal [traversal]
(loop [op-stack traversal arg-stack []]
(if (empty? op-stack)
(peek arg-stack)
(let [o (peek op-stack)
s (pop op-stack)]
(if-let [a (:arg o)]
(recur s (conj arg-stack a))
(let [[args op-args] (pop-n arg-stack (:count o))]
(recur s (conj args (apply (op-map (:op o)) op-args)))))))))
(defn eval-tree [tree] (-> tree build-traversal eval-traversal))
Se le puede llamar como tal:
user> (def t '(* (+ 1 2) (- 4 1 2) (/ 6 3)))
#'user/t
user> (eval-tree t)
6
lo dejo como ejercicio para el lector para convertir esto funcione con una estructura Antlr AST;)
Aunque me gustaría poder aceptar tanto esta respuesta como la siguiente que usa una pila, esta es realmente la mejor solución, incluso si no es exactamente lo que tenía en mente. ¡Gracias por dirigirme a una mejor forma de pensar acerca de este problema! –
FWIW, tree-seq da un recorrido de pre-pedido, no un pedido posterior. Honestamente, creo que la solución más simple aquí es 'clojure.walk/postwalk' y la usaría si fuera posible. Si realmente existe el peligro de volar la pila, entonces las cremalleras podrían funcionar, aunque puede ser complicado hacer un verdadero recorrido posterior a la orden con ellas. Independientemente de su idoneidad para este problema en particular, las cremalleras son una herramienta maravillosa para tener en su arsenal :) – Alex