@joinr, it’s really interesting what you’ve done.
I’ll put what I’ve extracted out from your example as a pseudo lisp targeting js (hopefully it’s pretty readable). Because JS is single threaded, I’m using a standard array for accumulation but on the JVM, either an atom or a concurrent deque would work.
The most important bit is the step function for nested-keys-step-fn
.
It takes three arguments → args
, queue
and accumulated output
It linearises a nested loop by taking an entry from a queue, doing an operation and putting more tasks on the queue if a child is an object:
(defn.js nested-keys-step-fn
[args queue out]
(var [path m] args)
(k/for:object [[k v] m]
(cond (k/obj? v)
;; add to queue if object
(x:arr-push queue [[(:.. path) k] v])
:else
;; add to accumulated output otherwise
(x:arr-push out [[(:.. path) k] v])))
(return [queue out]))
Examples:
(walk-step-fn [[] {"a" {"b" 2 "c" 3}}]
[]
[])
=> [[[["a"] {"b" 2, "c" 3}]] ;; queue
[]] ;; output
(walk-step-fn [["a"] {"b" 2, "c" 3}]
[]
[])
=> [[] ;; queue
[[["a" "b"] 2] [["a" "c"] 3]]] ;;output
Because the step function turns a nested loop into a linear one, we can use a generic walk
implementation (using iterators, transducers, parallell threads, whatever).
The simplest is a while loop:
(defn.js walk [step-fn queue]
(var out [])
(while (< 0 (k/len queue))
(var args (x:arr-pop queue))
(step-fn args queue out))
(return out))
and nested keys can be defined in terms of the walk function:
(defn.js nested-keys
[m]
(return (-/walk -/walk-step-fn [[[] m]])))
(nested-keys {:a {:b 1 :c 2}
:d {:e 1 :f 2}})
=> [[["d" "e"] 1]
[["d" "f"] 2]
[["a" "b"] 1]
[["a" "c"] 2]]
The simplest async version of walk on JS looks like this:
(defn.js walk-async [step-fn queue out]
(return (j/future
(var args (x:arr-pop queue))
(step-fn args queue out)
(if (< 0 (k/len queue))
(return (-/walk-async step-fn queue out))
(return out)))))
(defn.js nested-keys-async
[m]
(return (-/walk-async -/walk-step-fn [[[] m]] [])))
and output:
(j/<! (-/nested-keys-async {:a {:b 1 :c 2}
:d {:e 1 :f 2}}))
[[["d" "e"] 1] [["d" "f"] 2] [["a" "b"] 1] [["a" "c"] 2]]
I don’t think it blows the stack (because of the micro-tasklet implementation) but am not completely sure.