skip to Main Content

I’ve got this list of fields (that’s Facebook’s graph API fields list).

["a" "b" ["c" ["t"] "d"] "e" ["f"] "g"]

I want to generate a map out of it. The convention is following, if after a key vector follows, then its an inner object for the key. Example vector could be represented as a map as:

{"a" "value"
 "b" {"c" {"t" "value"} "d" "value"}
 "e" {"f" "value"}
 "g" "value"}

So I have this solution so far

(defn traverse
  [data]
  (mapcat (fn [[left right]]
            (if (vector? right)
              (let [traversed (traverse right)]
                (mapv (partial into [left]) traversed))
              [[right]]))
          (partition 2 1 (into [nil] data))))

(defn facebook-fields->map
  [fields default-value]
  (->> fields
       (traverse)
       (reduce #(assoc-in %1 %2 nil) {})
       (clojure.walk/postwalk #(or % default-value))))

(let [data ["a" "b" ["c" ["t"] "d"] "e" ["f"] "g"]]
  (facebook-fields->map data "value"))
#=> {"a" "value", "b" {"c" {"t" "value"}, "d" "value"}, "e" {"f" "value"}, "g" "value"}

But it is fat and difficult to follow. I am wondering if there is a more elegant solution.

3

Answers


  1. Here’s another way to do it using postwalk for the whole traversal, rather than using it only for default-value replacement:

    (defn facebook-fields->map
      [fields default-value]
      (clojure.walk/postwalk
        (fn [v] (if (coll? v)
                  (->> (partition-all 2 1 v)
                       (remove (comp coll? first))
                       (map (fn [[l r]] [l (if (coll? r) r default-value)]))
                       (into {}))
                  v))
        fields))
    
    (facebook-fields->map ["a" "b" ["c" ["t"] "d"] "e" ["f"] "g"] "value")
    => {"a" "value",
        "b" {"c" {"t" "value"}, "d" "value"},
        "e" {"f" "value"},
        "g" "value"}
    
    Login or Signup to reply.
  2. Trying to read heavily nested code makes my head hurt. It is worse when the answer is something of a “force-fit” with postwalk, which does things in a sort of “inside out” manner. Also, using partition-all is a bit of a waste, since we need to discard any pairs with two non-vectors.

    To me, the most natural solution is a simple top-down recursion. The only problem is that we don’t know in advance if we need to remove one or two items from the head of the input sequence. Thus, we can’t use a simple for loop or map.

    So, just write it as a straightforward recursion, and use an if to determine whether we consume 1 or 2 items from the head of the list.

    1. If the 2nd item is a value, we consume one item and add in
      :dummy-value to make a map entry.
    2. If the 2nd item is a vector, we recurse and use that
      as the value in the map entry.

    The code:

    (ns tst.demo.core
      (:require [clojure.walk :as walk] ))
    
    (def data ["a" "b" ["c" ["t"] "d"] "e" ["f"] "g"])
    
    (defn parse [data]
      (loop [result {}
             data   data]
        (if (empty? data)
          (walk/keywordize-keys result)
          (let [a (first data)
                b (second data)]
            (if (sequential? b)
              (recur
                (into result {a (parse b)})
                (drop 2 data))
              (recur
                (into result {a :dummy-value})
                (drop 1 data)))))))
    

    with result:

    (parse data) => 
    {:a :dummy-value,
     :b {:c {:t :dummy-value}, :d :dummy-value},
     :e {:f :dummy-value},
     :g :dummy-value}
    

    I added keywordize-keys at then end just to make the result a little more “Clojurey”.

    Login or Signup to reply.
  3. Since you’re asking for a cleaner solution as opposed to a solution, and because I thought it was a neat little problem, here’s another one.

    (defn facebook-fields->map [coll]
      (into {}
            (keep (fn [[x y]]
                    (when-not (vector? x)
                      (if (vector? y)
                        [x (facebook-fields->map y)]
                        [x "value"]))))
            (partition-all 2 1 coll)))
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search