0

Given a binding form such as [{a :a} {b :b}] how can I find all the symbols? (a b)

Timothy Pratley
  • 10,586
  • 3
  • 34
  • 63

2 Answers2

2

The naïve approach would be to just treat the binding form as some nested collection, find all the symbols in that collection, and return the sequence of those symbols:

(defn symbols [x]
  (filter symbol? (tree-seq coll? seq x)))

(symbols '[{a :a} {b :b}])
;;=> (a b)

However, as @amalloy noted, this won't work in all cases. Here are some examples where the above implementation of symbols gives an undesirable result:

;; & isn't actually bound to anything
(symbols '[foo & bar])
;;=> (foo & bar)

;; duplicates
(symbols '{x :foo :or {x :bar}})
;;=> (x x)

;; keys and default values are evaluated, not bound
(symbols '{x (keyword "foo") :or {x (keyword 'bar)}})
;;=> (x keyword x keyword quote bar)

;; namespaced keywords and symbols don't work
(symbols '{:keys [::foo :bar/baz qux/quux]})
;;=> (qux/quux)

He suggests using the built-in destructure function instead, but as he demonstrated in his answer, this causes some garbage to show up in the result:

(take-nth 2 (destructure '[{:keys [x]} (last y)]))
;;=> (map__10938 map__10938 x)

While this technically gives the list of symbols that Clojure will bind, that map__10938 is just an implementation artifact, and has nothing to do with the destructuring language itself.

Thankfully, it's not too hard to parse the binding form manually and assemble a set of the symbols, taken from the original binding form, that would be bound:

(require '[clojure.set :as set])

(defn symbols [binding]
  (cond
    (symbol? binding)
    #{binding}

    (vector? binding)
    (apply set/union (map symbols (remove #{'& :as} binding)))

    (map? binding)
    (apply set/union
           (for [[k v] binding]
             (case k
               :or #{}
               :as #{v}
               (:keys :strs :syms) (set (map (comp symbol name) v))
               (symbols k))))))
Sam Estep
  • 12,974
  • 2
  • 37
  • 75
  • This looks pretty good to me. You can simplify your `cond` quite a bit, to `(case k :or #{}, :as #{v}, (:keys :strs :syms) (set ...), (symbols k))` – amalloy Jul 01 '16 at 02:36
  • @amalloy Cool, thanks! :) I didn't know you could do that with `case`. – Sam Estep Jul 01 '16 at 02:55
1

Much better is to use clojure.core/destructure, which understands which symbols are names that will be bound, rather than values that will be taken apart. For example, consider:

(let [{:keys [x]} (last y)]
  x)

In that context you almost certainly don't want to include last in the list of symbols, presuming that you are using this to better understand a destructuring spec. And if you call destructure, it tells you exactly what names will be bound to what values:

user> (destructure '[{:keys [x]} (last y)])
[map__10938 (last y)
 map__10938 (if (clojure.core/seq? map__10938)
              (clojure.lang.PersistentHashMap/create (clojure.core/seq map__10938)) 
              map__10938) 
 x (clojure.core/get map__10938 :x)]

Now on the one hand you are getting a symbol that wasn't actually typed in by the caller, but that's probably still useful, because it tells you what Clojure will actually do to handle this let expression. To get just the left-hand side, ie the names that will be bound, you can use

(take-nth 2 (destructure '[{:keys [x]} (last y)]))
amalloy
  • 89,153
  • 8
  • 140
  • 205
  • He's not asking to look at both halves of a binding (e.g. `[{:keys [x]} (last y)]`); he only wants to find the symbols in the *left* side of the binding. For instance, an expression might look like `(let [[{a :a} {b :b}] [{:a :foo} {:b :bar}]] [a b])`, but as he stated in his question, he only wants to look for symbols in the `[{a :a} {b :b}]` part. – Sam Estep Jun 30 '16 at 19:27
  • Even if you only have the left half, some symbols can sneak in that are not names that will be bound. For example, `{:keys [x] :or {x (range 10)}}` - do you really want `range` returned from this function? – amalloy Jun 30 '16 at 20:27
  • Ah, cool! I went back over the [Clojure destructuring guide](http://clojure.org/guides/destructuring), and it looks like there's a lot of stuff you can do with destructuring that I didn't know about. I edited my answer to reflect those complexities. – Sam Estep Jul 01 '16 at 02:34