Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is use of clojure.spec for coercion idiomatic?

I've seen use of clojure conformers to coerce data in various gists, but have also picked up an impression (I can't recall where though) that coercion (e.g. as follows) is not idiomatic use of conformers.

(s/def :conformers/int
  (s/conformer (fn [x]
                 (cond
                  (integer? x) x
                  (re-matches #"\d+" x) (edn/read-string x)
                 :else :cljs.spec.alpha/invalid))))

(s/def :data.subscription/quantity :conformers/int)

(s/def :data/subscription (s/keys :req-un [:data.subscription/quantity]))

Is it true that the above is unidiomatic/unintended? And if so, what would be appropriate/idiomatic use. Where do the boundaries of the intended use lie?

like image 921
Daniel Neal Avatar asked Jul 19 '17 11:07

Daniel Neal


3 Answers

UPDATE:

Now I have published a library to handle this, please check it out at: https://github.com/wilkerlucio/spec-coerce


You can use spec for coercion, but it's important that you also have the non-coercive version of it. If you force the coercion on your specs you are doing 2 things at the same time, violating SRP. So the recommendation is having a simple validation one, and then you can another one on top of it, so later you can choose if you want to use the coercive version or the simple validation one.

Another option (that I prefer) is to have a coercion engine based on the specs running in parallel. If you look at how specs infer the generator from the specs (check here), you can see that you can use the spec form to derive something else, so this something else can be your coercion engine.

I have written one article where I explain how to do that, you can find it here (just jumping to the coercing with specs section): https://medium.com/@wilkerlucio/implementing-custom-om-next-parsers-f20ca6db1664

Code extracted from the article for reference:

(def built-in-coercions
  {`int?     #(Long/parseLong %)
   `nat-int? #(Long/parseLong %)
   `pos-int? #(Long/parseLong %)
   `inst?    clojure.instant/read-instant-timestamp})

(defn spec->coerce-sym [spec]
  (try (s/form spec) (catch Exception _ nil)))

(defn coerce [key value]
  (let [form (spec->coerce-sym key)
        coerce-fn (get built-in-coercions form identity)]
    (if (string? value)
      (coerce-fn value)
      value)))

Also here is a more elaborated version of it (just code), that includes a secondary registry so you can set specific coercers to match the same spec keyword: https://gist.github.com/wilkerlucio/08fb5f858a12b86f63c76cf453cd90c0

This way you don't enforce the coercion, making your validations faster and giving you more control over when to coerce (which usually should only happen at the borders of your system).

like image 53
Wilker Lucio Avatar answered Nov 08 '22 09:11

Wilker Lucio


While specs design is still being fleshed out its a bit too early for a definitive answer. So I use this definition, derived from how the standard lib uses conforming:


Coercion is implicit and automatic conversion to the shape expected downstream.

Conforming takes a value that already has the expected shape and produces programmatic information derived from the value and the spec together (hence conform). The conform result is not guaranteed to be valid according to the spec. Examples are s/or based specs or Regex based specs.


In short: Conforming is not idiomatic coercion but something else instead that looks similar.

I'd expect coercion in spec to become a separate feature sometime.

like image 33
Leon Grapenthin Avatar answered Nov 08 '22 09:11

Leon Grapenthin


Clojure.spec is not intended for coercion

Per the team that writes clojure.spec, it is not idiomatic to use it for coercion. Proceed at your own design and engineering risk.

Alex Miller, of the Cognitect clojure.core team, reiterates the official position on the Clojure mailing list 20 February 2018:

We recommend that you not use conformers for coercion.

He hints to their reasons why: later in the same thread, he says of a library that builds coercion on top of spec that it "combines specs for your desired output with a coercion function, making the spec of the actual data implicit." This conflation is not part of the intended usage of clojure.spec.

But...how to coerce, if not with spec? The answer is, plain old Clojure functions, just like we've been doing all along. Again from Alex Miller (16 December 2016):

If you really need to do a wholesale coercion of all your attributes, it seems like you can do this explicitly as a pre-step before validating the map with spec and that may be the better way to go. I'm not sure what spec is buying you over just explicitly transforming the map using normal Clojure functions?

...why not?

Programming contracts such as spec represent an agreement between parties that is used at system boundaries. These specs/contracts/agreements are intended to be used for validation and error checking, testing (especially generative), and documentation (especially upon errors). Agreements about what data should be are definitely not the same as the act of turning data that is not conformant into data that is conformant. They're two different concerns, even though they might often occur near each other. This adjacency of the two concepts makes it particularly important not to muddle them.

like image 4
Dave Liepmann Avatar answered Nov 08 '22 09:11

Dave Liepmann