你将如何尾调用优化这个构建树的 Clojure 函数?
How would you tail call optimize this Clojure function that builds a tree?
我有一些地图
(def m1 [{:a 1, :b 2, :c 0}
{:a 1, :b 3, :c 0}
{:a 1, :b 0, :c 2}
{:a 1, :b 3, :c 1}
{:a 1, :b 0, :c 3}])
我可以用这个函数递归分组
(defn group [ks coll]
(if (empty? ks) coll
(let [gs (group-by #(select-keys % [(first ks)]) coll)]
(map (fn [[k v]] {k (group (rest ks) v)}) (dissoc gs {})))))
产生预期结果:
(group [:a :b :c] m1)
=>
({{:a 1} ({{:b 2} ({{:c 0} [{:a 1, :b 2, :c 0}]})}
{{:b 3} ({{:c 0} [{:a 1, :b 3, :c 0}]}
{{:c 1} [{:a 1, :b 3, :c 1}]})}
{{:b 0} ({{:c 2} [{:a 1, :b 0, :c 2}]}
{{:c 3} [{:a 1, :b 0, :c 3}]})})})
你怎么能重写这样一个在最后位置有 map
的函数,因为它需要遵循多个路径,以便使用 recur
进行尾调用优化?
好吧,事实证明它非常复杂。我设法使用 loop/recur
实现了类似上面的内容,但我不得不引入一堆辅助函数以使其具有一定的可读性:
(defn group-recur [ks coll]
(let [split-map (fn [m]
(->> m
(into [])
(map (partial apply hash-map))))
dissoc-non-map (fn [m k]
(let [l (drop-last k)]
(if-not (map? (get-in m k))
(if (empty? l)
(dissoc m (last k))
(update-in m l dissoc (last k)))
m)))
updater (fn [r k v]
(let [s (split-map k)]
(-> r
(dissoc-non-map (drop-last s))
(assoc-in s v))))]
(loop [k ks
h []
r {}]
(if (not-empty k)
(let [all (conj h (first k))
grouped (dissoc (group-by #(select-keys % all) coll) {})]
(->> grouped
(reduce-kv updater r)
(recur (rest k)
all)))
r))))
它 returns 一棵地图树,而不是您上面使用的集合(我认为这更合乎逻辑)。基本前提是您传递结果地图并在传递过程中更新它。这就是 dissoc-non-map
在那里的原因,显然如果在更高级别上已经存在另一个非地图值,则您不能在地图中 assoc-in
。
循环需要 3 个参数:仍待处理的键列表、我们当前处理的 key/value 对(即当前的 "position in the tree",如果你愿意的话)和生成的映射树.
另一种(可能更简单的)实现可以传递两个映射:一个是最终结果,另一个是中间值。那应该消除对 dissoc-non-map
.
的需要
你原来的算法要求在递归的时候把当前的兄弟结点作为context保存下来,这样我们可以return在递归到底层之后再去处理那些兄弟结点。通过稍微修改,我们可以一次构建到一个节点的完整路径,深度优先,忽略节点之间的关系。这允许更简单的尾调用递归。
(defn group [ks coll]
(group-recur ks coll ks [] {}))
(defn- group-recur [ks coll recur-keys path acc]
(if (empty? coll)
acc
(if (empty? recur-keys)
(recur ks (rest coll) ks [] (assoc-in acc path (first coll)))
(let [node (first coll)
rk (first recur-keys)
k (select-keys node [rk])
p (conj path k)]
(recur ks coll (rest recur-keys) p acc)))))
这里有一个 online Repl 来试试看。
我有一些地图
(def m1 [{:a 1, :b 2, :c 0}
{:a 1, :b 3, :c 0}
{:a 1, :b 0, :c 2}
{:a 1, :b 3, :c 1}
{:a 1, :b 0, :c 3}])
我可以用这个函数递归分组
(defn group [ks coll]
(if (empty? ks) coll
(let [gs (group-by #(select-keys % [(first ks)]) coll)]
(map (fn [[k v]] {k (group (rest ks) v)}) (dissoc gs {})))))
产生预期结果:
(group [:a :b :c] m1)
=>
({{:a 1} ({{:b 2} ({{:c 0} [{:a 1, :b 2, :c 0}]})}
{{:b 3} ({{:c 0} [{:a 1, :b 3, :c 0}]}
{{:c 1} [{:a 1, :b 3, :c 1}]})}
{{:b 0} ({{:c 2} [{:a 1, :b 0, :c 2}]}
{{:c 3} [{:a 1, :b 0, :c 3}]})})})
你怎么能重写这样一个在最后位置有 map
的函数,因为它需要遵循多个路径,以便使用 recur
进行尾调用优化?
好吧,事实证明它非常复杂。我设法使用 loop/recur
实现了类似上面的内容,但我不得不引入一堆辅助函数以使其具有一定的可读性:
(defn group-recur [ks coll]
(let [split-map (fn [m]
(->> m
(into [])
(map (partial apply hash-map))))
dissoc-non-map (fn [m k]
(let [l (drop-last k)]
(if-not (map? (get-in m k))
(if (empty? l)
(dissoc m (last k))
(update-in m l dissoc (last k)))
m)))
updater (fn [r k v]
(let [s (split-map k)]
(-> r
(dissoc-non-map (drop-last s))
(assoc-in s v))))]
(loop [k ks
h []
r {}]
(if (not-empty k)
(let [all (conj h (first k))
grouped (dissoc (group-by #(select-keys % all) coll) {})]
(->> grouped
(reduce-kv updater r)
(recur (rest k)
all)))
r))))
它 returns 一棵地图树,而不是您上面使用的集合(我认为这更合乎逻辑)。基本前提是您传递结果地图并在传递过程中更新它。这就是 dissoc-non-map
在那里的原因,显然如果在更高级别上已经存在另一个非地图值,则您不能在地图中 assoc-in
。
循环需要 3 个参数:仍待处理的键列表、我们当前处理的 key/value 对(即当前的 "position in the tree",如果你愿意的话)和生成的映射树.
另一种(可能更简单的)实现可以传递两个映射:一个是最终结果,另一个是中间值。那应该消除对 dissoc-non-map
.
你原来的算法要求在递归的时候把当前的兄弟结点作为context保存下来,这样我们可以return在递归到底层之后再去处理那些兄弟结点。通过稍微修改,我们可以一次构建到一个节点的完整路径,深度优先,忽略节点之间的关系。这允许更简单的尾调用递归。
(defn group [ks coll]
(group-recur ks coll ks [] {}))
(defn- group-recur [ks coll recur-keys path acc]
(if (empty? coll)
acc
(if (empty? recur-keys)
(recur ks (rest coll) ks [] (assoc-in acc path (first coll)))
(let [node (first coll)
rk (first recur-keys)
k (select-keys node [rk])
p (conj path k)]
(recur ks coll (rest recur-keys) p acc)))))
这里有一个 online Repl 来试试看。