Fork me on GitHub
#clojure-spec
<
2016-06-03
>
jcf11:06:18

I'm having some trouble spec'ing a fn that walks maps allowing you to hyphenate keys etc. I wonder if someone can point out my mistake as I'm having a hard time decrypting the error message…

(defn walk-map
  "Recursively apply a function to all map entries. When map is nil returns an
  empty map."
  [f m]
  (if m
    (walk/postwalk (fn [x] (if (map? x) (into {} (map f x)) x)) m)
    {}))

(s/fdef walk-map
  :args (s/cat :f (s/fspec :args (s/tuple ::s/any ::s/any) :ret ::kv)
               :m ::maybe-any-map)
  :ret ::any-map)

(defn hyphenate-keys
  "Recursively transforms all map keys from underscored strings to hyphenated
  keywords."
  [m]
  (walk-map (fn [[k v]] [(hyphenated-keyword k) v]) m))

(s/fdef hyphenate-keys
  :args (s/cat :m ::maybe-any-map)
  :ret ::any-map)

jcf11:06:27

The error:

ERROR in (t-hyphenate-keys) (core.clj:4631)
expected: (= (sut/hyphenate-keys {:a 1, "a" 2}) {:a 2})
  actual: clojure.lang.ExceptionInfo: Call to #'example.common/walk-map did not conform to spec:
In: [0] val: ({}) fails at: [:args :f] predicate: (apply fn),  nth not supported on this type: PersistentArrayMap
:clojure.spec/args  (#object[example.common$hyphenate_keys$fn__20568 0x754a38a0 "example.common$hyphenate_keys$fn__20568@754a38a0"] {:a 1, "a" 2})

jcf11:06:08

I've tried a s/cat to capture ::s/any but I'm sure I'm missing something.

jcf11:06:24

With the s/cat that I'd expect I need:

ERROR in (t-walk-map) (core.clj:4631)
expected: (= (sut/walk-map (fn [[k v]] [(name k) (if (number? v) (inc v) v)]) {:a 1, :b {:c 2, :d {:e 3}}}) {"a" 2, "b" {"c" 3, "d" {"e" 4}}})
  actual: clojure.lang.ExceptionInfo: Call to #'example.common/walk-map did not conform to spec:
In: [0] val: ([[] []]) fails at: [:args :f] predicate: (apply fn),  clojure.lang.PersistentVector cannot be cast to clojure.lang.Named
:clojure.spec/args  (#object[example.common_test$fn__21912$fn__21926 0x7787a602 "example.common_test$fn__21912$fn__21926@7787a602"] {:a 1, :b {:c 2, :d {:e 3}}})

jcf11:06:49

(s/fdef walk-map
  :args (s/cat :f (s/fspec :args (s/cat :kv ::s/any) :ret ::kv)
               :m ::maybe-any-map)
  :ret ::any-map)
Hmm.

jcf11:06:13

Shouldn't an fspec with :args (s/cat :kv ::s/any) match any and all args?

jcf12:06:51

This is what's confusing me. This spec looks right, but doesn't work in the fspecs :args.

(s/explain-data (s/cat :kv (s/tuple ::s/any ::s/any)) [[:a {:b 2}]])

manutter5112:06:54

what is your hyphenated-keyword fn doing? I’m not sure I’m following what’s happening there, but it looks like it’s blowing up in that function, and that’s causing the spec failure

jcf12:06:15

(defn- hyphenated-keyword
  [x]
  (if (or (string? x) (keyword? x))
    (-> x keyword->string infl/hyphenate keyword)
    x))
@manutter51: I've used this code in a number of projects for a few years now. The fns are pretty reliable.

jcf12:06:27

The relevant specs I'm using:

(s/def ::any-map
  (s/map-of (s/nilable ::s/any) (s/nilable ::s/any)))

(s/def ::maybe-any-map
  (s/nilable ::any-map))

jcf12:06:58

Not sure if ::s/any is already nilable… 🙂

jcf12:06:21

In the walk-map test I get a cast exception:

ERROR in (t-walk-map) (core.clj:4631)
expected: (= (sut/walk-map (fn [[k v]] [(name k) (if (number? v) (inc v) v)]) {:a 1, :b {:c 2, :d {:e 3}}}) {"a" 2, "b" {"c" 3, "d" {"e" 4}}})
  actual: clojure.lang.ExceptionInfo: Call to #'example.common/walk-map did not conform to spec:
In: [0] val: ([[] []]) fails at: [:args :f] predicate: (apply fn),  clojure.lang.PersistentVector cannot be cast to clojure.lang.Named
:clojure.spec/args  (#object[example.common_test$fn__21912$fn__21926 0x5ea46e79 "example.common_test$fn__21912$fn__21926@5ea46e79"] {:a 1, :b {:c 2, :d {:e 3}}})
In the tests that rely on walk-map:
ERROR in (t-underscore-keys) (core.clj:4631)
expected: (= (sut/underscore-keys {"a-b" 1}) {"a_b" 1})
  actual: clojure.lang.ExceptionInfo: Call to #'example.common/walk-map did not conform to spec:
In: [0] val: ([[] []]) fails at: [:args :f] predicate: (apply fn)
:clojure.spec/args  (#object[example.common$underscore_keys$fn__20550 0x1f36bed9 "example.common$underscore_keys$fn__20550@1f36bed9"] {"a-b" 1})

jcf12:06:12

These error messages aren't intuitive. I've got a feeling this will be another thing newcomers to Clojure really struggle with.

jcf12:06:58

It recurses into a ::pcat to expand out the error explanation, and I think I'm looking at a ::pcat above.

jcf12:06:38

Okay, so only the commented out test fails:

(deftest t-walk-map
  (are [f m x] (= (sut/walk-map f m) x)
    identity nil    {}
    identity {}     {}
    identity {:a 1} {:a 1}

    ;; (fn [[k ^long v]] [(name k) (if (number? v) (inc v) v)])
    ;; {:a 1 :b {:c 2 :d {:e 3}}}
    ;; {"a" 2 "b" {"c" 3 "d" {"e" 4}}}
    ))

jcf12:06:34

Okay. Looks like spec was exercising my walk-map with keys like [] and (). I was then calling name on vectors etc.

jcf12:06:08

Fixed that by making my test fn valid. 🙂

(deftest t-walk-map
  (are [f m x] (= (sut/walk-map f m) x)
    identity nil    {}
    identity {}     {}
    identity {:a 1} {:a 1}

    (fn [[k v]]
      [(if (named? k) (name k) k)
       (if (number? v) (inc ^long v) v)])
    {:a 1 :b {:c 2 :d {:e 3}}}
    {"a" 2 "b" {"c" 3 "d" {"e" 4}}}))

jcf12:06:43

One down. One to go!

ERROR in (t-underscore-keys) (core.clj:4631)
expected: (= (sut/underscore-keys nil) {})
  actual: clojure.lang.ExceptionInfo: Call to #'example.common/walk-map did not conform to spec:
In: [0] val: ([[] []]) fails at: [:args :f] predicate: (apply fn)
:clojure.spec/args  (#object[example.common$underscore_keys$fn__20550 0x6dd8e64b "example.common$underscore_keys$fn__20550@6dd8e64b"] nil)

jcf12:06:31

I wonder if it's the same problem. I need to make sure I have named keys.

jcf14:06:49

It was. Because I had enabled instrumentation my functions were being called with vectors, sets, etc. and those weren't supported.

benzap14:06:41

so i'm converting from schema, and i'm having some issues with respect to preventing clutter within my namespace. maybe i'm not structuring my data correctly

benzap14:06:17

ex. `(def TextBlock {:type (schema/eq "Text") :foreground-color Color :background-color Color :style {(schema/optional-key :bold) schema/Bool (schema/optional-key :underline) schema/Bool (schema/optional-key :italic) schema/Bool} :text Letter}) (s/def ::foreground-color ::color) (s/def ::background-color ::color) (s/def ::style (s/keys :opt-un [::bold ::underline ::italic])) (s/def ::textblock (s/keys :req-un [::type ::foreground-color ::background-color ::style]))`

benzap14:06:00

how would I apply s/def to ::bold, ::underline and ::italic, without s/def? I don't want those in the namespace

benzap14:06:49

if I did have them in the namespace, i'd prefer to have them called something like ::font-style-underline, but the underlying spec would need to accept :underline as the style key

benzap14:06:39

Is there any way to do this? I'm confused on whether I should be inlining the spec stuff. I'm sticking to what I did with plumatic.schema by placing all of my schemas in one file called schemas.cljs

jcf15:06:34

@benzap: I don't think you can. You need to def the keywords in order to enable reuse.

jcf15:06:33

And I'd think you only want really common schema in a global namespace as spec is embracing namespaced keywords.

jcf15:06:26

So, :font/bold and :font/underline instead of :schemas/font-style-underline etc.

jcf15:06:23

I've used specs like :common/any-map because I want nilable maps in a lot of places, but there might be a better way I haven't yet found.

jcf15:06:01

(And I've done away completely with my shared schema namespaces for now.)

benzap15:06:38

@jcf: I see, so I guess i'll have to start embracing namespaced keywords as well 🙂

benzap15:06:26

I already have a font.cljs, would it be common to place spec in-line with the code?

jcf15:06:15

That's what I'm doing, and what I've seen in all the examples.

jcf15:06:06

It makes sense keeping everything starting with font in font.cljs; that's where I'd look at least!

nwjsmith19:06:22

Hrm, is there a way to specify a transform to a spec's conformed data?

nwjsmith19:06:33

I'm speccing datomic query, which accepts either a list or a map, and I'd like them to conform the same way

nwjsmith19:06:45

Found it! Looks like clojure.spec/conformer is what I was after.

hiredman21:06:24

I haven't seen this discussed elsewhere, but as a warning, protocol functions called in a non-higher order way (where clojure knows the protocol function is being called an creates an optimized call site for it) don't properly check args and returns against specs if instrumented