Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Clojure.spec - Why is it useful and when is it used

I have recently watched Rich Hickeys talk at Cojure Conj 2016 and although it was very interesting, I didn't really understand the point in clojure.spec or when you'd use it. It seemed like most of the ideas, such as conform, valid etc, had similar functions in Clojure already.

I have only been learning clojure for around 3 months now so maybe this is due to lack of programming/Clojure experience.

Do clojure.spec and cljs.spec work in similar ways to Clojure and Cljs in that, although they are not 100% the same, they are based on the same underlying principles.

like image 853
rbb Avatar asked Dec 11 '16 16:12

rbb


2 Answers

  • Are you tired of documenting your programs?
  • Does the prospect of making up yet more tests cause procrastination?
  • When the boss says "test coverage", do you cower with fear?
  • Do you forget what your data names mean?

For smooth expression of hard specifications, you need Clojure.Spec!


Clojure.spec gives you a uniform method of documenting, specifying, and automatically testing your programs, and of validating your live data.

It steals virtually every one of its ideas. And it does nothing you can't do for yourself.

But in my - barely informed - opinion, it changes the economy of specification, making it worth while doing properly. A game-changer? - quite possibly.

like image 85
Thumbnail Avatar answered Sep 22 '22 21:09

Thumbnail


At the clojure/conj conference last week, probably half of the presentations featured spec in some way, and it's not even out of alpha yet. spec is a major feature of clojure; it is here to stay, and it is powerful.

As an example of its power, take static type checking, hailed as a kind of safety net by so many, and a defining characteristic of so many programming languages. It is incredibly limited in that it's only good at compile time, and it only checks types. spec, on the other hand, validates and conforms any predicate (not just type) for the args, the return, and can also validate relationships between the two. All of this is external to the function's code, separating the logic of the function from being commingled with validation and documentation about the code.

Regarding WORKFLOW:

One archetypal example of the benefits of relationship-checking, versus only type-checking, is a function which computes the substring of a string. Type checking ensures that in (subs s start end) the s is a string and start and end are integers. However, additional checking must be done within the function to ensure that start and end are positive integers, that end is greater than start, and that the resulting substring is no larger than the original string. All of these things can be spec'd out, for example (forgive me if some of this is a bit redundant or maybe even inaccurate):

(s/fdef clojure.core/subs

        :args (s/and (s/cat :s string? :start nat-int? :end (s/? nat-int?))
                     (fn [{:keys [s start end]}]
                       (if end
                         (<= 0 start end (count s))
                         (<= 0 start (count s)))))

        :ret string?

        :fn (fn [{{:keys [s start end]} :args, substring :ret}]
              (and (if end
                     (= (- end start) (count substring))
                     (= (- (count s) start) (count substring)))
                   (<= (count substring) (count s)))))

Call the function with sample data meeting the above args spec:

(s/exercise-fn `subs)

Or run 1000 tests (this may fail a few times, but keep running and it will work--this is due to the built-in generator not being able to satisfy the second part of the :args predicate; a custom generator can be written if needed):

(stest/check `subs)

Or, want to see if your app makes calls to subs that are invalid while it's running in real time? Just run this, and you'll get a spec exception if the function is called and the specs are not met:

(stest/instrument `subs)

We have not integrated this into our work flow yet, and can't in production since it's still alpha, but the first goal is to write specs. I'm putting them in the same namespace but in separate files currently.

I foresee our work flow being to run the tests for spec'd functions using this (found in the clojure spec guide):

(-> (stest/enumerate-namespace 'user) stest/check)

Then, it would be advantageous to turn on instrumenting for all functions, and run the app under load as we normally would test it, and ensure that "real world" data works.

You can also use s/conform to destructure complex data in functions themselves, or use s/valid as pre- and post- conditions for running functions. I'm not too keen on this, as it's overhead in a production system, but it is a possibility.

The sky's the limit, and we've just scratched the surface! Cool things coming in the next months and years with spec!

like image 30
Josh Avatar answered Sep 20 '22 21:09

Josh