Как совпадение, я работал над кодом манипуляции AST в в библиотеке Тупело Форест . Вы можете посмотреть пример кода здесь и видео с Clojure / Conj 2017 здесь .
Ниже показано, как бы я решил эту проблему. Я попытался сделать имена как можно более очевидными, чтобы было легко понять, как работает алгоритм.
Основа:
(def op->arity {:add 2
:sub 2
:mul 2
:div 2
:pow 2})
(def op-set (set (keys op->arity)))
(defn choose-rand-op [] (rand-elem op-set))
(def arg-set #{:x :y})
(defn choose-rand-arg [] (rand-elem arg-set))
(defn num-hids [] (count (all-hids)))
Вспомогательные функции:
(s/defn hid->empty-kids :- s/Int
[hid :- HID]
(let [op (hid->attr hid :op)
arity (grab op op->arity)
kid-slots-used (count (hid->kids hid))
result (- arity kid-slots-used)]
(verify (= 2 arity))
(verify (not (neg? result)))
result))
(s/defn node-has-empty-slot? :- s/Bool
[hid :- HID]
(pos? (hid->empty-kids hid)))
(s/defn total-empty-kids :- s/Int
[]
(reduce +
(mapv hid->empty-kids (all-hids))))
(s/defn add-op-node :- HID
[op :- s/Keyword]
(add-node {:tag :op :op op} )) ; add node w no kids
(s/defn add-leaf-node :- tsk/KeyMap
[parent-hid :- HID
arg :- s/Keyword]
(kids-append parent-hid [(add-leaf {:tag :arg :arg arg})]))
(s/defn need-more-op? :- s/Bool
[tgt-size :- s/Int]
(let [num-op (num-hids)
total-size-so-far (+ num-op (total-empty-kids))
result (< total-size-so-far tgt-size)]
result))
Основной алгоритм:
(s/defn build-rand-ast :- tsk/Vec ; bush result
[ast-size]
(verify (<= 3 ast-size)) ; 1 op & 2 args minimum; #todo refine this
(with-debug-hid
(with-forest (new-forest)
(let [root-hid (add-op-node (choose-rand-op))] ; root of AST
; Fill in random op nodes into the tree
(while (need-more-op? ast-size)
(let [node-hid (rand-elem (all-hids))]
(when (node-has-empty-slot? node-hid)
(kids-append node-hid
[(add-op-node (choose-rand-op))]))))
; Fill in random arg nodes in empty leaf slots
(doseq [node-hid (all-hids)]
(while (node-has-empty-slot? node-hid)
(add-leaf-node node-hid (choose-rand-arg))))
(hid->bush root-hid)))))
(defn bush->form [it]
(let [head (xfirst it)
tag (grab :tag head)]
(if (= :op tag)
(list (kw->sym (grab :op head))
(bush->form (xsecond it))
(bush->form (xthird it)))
(kw->sym (grab :arg head)))))
(dotest
(let [tgt-size 13]
(dotimes [i 5]
(let [ast (build-rand-ast tgt-size)
res-str (pretty-str ast)]
(nl)
(println res-str)
(println (pretty-str (bush->form ast))) ))))
Он печатает результаты как в виде иероглифа "кустарник", так и в виде шрифтов. Вот 2 типичных результата:
[{:tag :op, :op :mul}
[{:tag :op, :op :div}
[{:tag :op, :op :pow}
[{:tag :op, :op :sub}
[{:tag :arg, :arg :y, :value nil}]
[{:tag :arg, :arg :x, :value nil}]]
[{:tag :op, :op :div}
[{:tag :arg, :arg :y, :value nil}]
[{:tag :arg, :arg :y, :value nil}]]]
[{:tag :arg, :arg :y, :value nil}]]
[{:tag :op, :op :pow}
[{:tag :arg, :arg :x, :value nil}]
[{:tag :arg, :arg :y, :value nil}]]]
(mul (div (pow (sub y x) (div y y)) y) (pow x y))
[{:tag :op, :op :div}
[{:tag :op, :op :mul}
[{:tag :op, :op :pow}
[{:tag :arg, :arg :x, :value nil}]
[{:tag :arg, :arg :y, :value nil}]]
[{:tag :op, :op :add}
[{:tag :op, :op :div}
[{:tag :arg, :arg :x, :value nil}]
[{:tag :arg, :arg :y, :value nil}]]
[{:tag :arg, :arg :x, :value nil}]]]
[{:tag :op, :op :mul}
[{:tag :arg, :arg :x, :value nil}]
[{:tag :arg, :arg :y, :value nil}]]]
(div (mul (pow x y) (add (div x y) x)) (mul x y))
Для простоты я использовал трехбуквенные операционные коды вместо математических символов, но их можно было легко заменить именами символов функции Clojure для ввода в eval
.