0

I am super confused by Clojure Spec. When I run in the repl by entering:

(require '[clojure.spec.alpha :as s])

And then add:

(s/valid? even? 10)

I get //true. And when I run:

(s/valid? even? 11)

//False. Ok so that works. Then when I require spec in my core.clj as:

(ns spam-problem.core
    (:require [clojure.spec.alpha :as s]
              [clojure.spec.gen.alpha :as gen]))

And try a simple validation to get it to throw an error, nothing happens:

(defn -main
  "I don't do a whole lot ... yet."
  [& args]
  (s/valid? even? 11))

I have no idea what I'm doing wrong here and am very confused about how spec is supposed to work. I am running this by using command lein run. Is there another way you're supposed to run it?

3
  • 2
    valid doesn't throw. It returns a Boolean value, which you're not doing anything with. Commented Dec 15, 2017 at 23:41
  • Ok, but also does not work with conform. I should have used that in my example instead. Commented Dec 15, 2017 at 23:42
  • Conform also doesn’t throw. You may want to use s/assert instead. Commented Dec 16, 2017 at 10:01

2 Answers 2

1

I understand what you are feeling because once I got into Spec it caused me the same thoughts. What really helped me to solve the problem in my mind is to considering Spec being not a final library but rather a framework. In my projects, usually I've got a special module with high-level wrappers above basic spec capabilities. I believe, you might do the same: define a function that takes data, spec and raises those error message you desire to have in terms of your business-logic. Here is a small example of my code:

(ns project.spec
  (:require [clojure.spec.alpha :as s]))

;; it's better to define that value is a constant
(def invalid :clojure.spec.alpha/invalid)

(defn validate
  "Either returns coerced data or nil in case of error."
  [spec value]
  (let [result (s/conform spec value)]
    (if (= result invalid)
      nil
      result)))

(defn spec-error
  "Returns an error map for data structure that does not fit spec." 
  [spec data]
  (s/explain-data spec data))

Now, let's prepare some specs:

(defn x-integer? [x]
  (if (integer? x)
    x
    (if (string? x)
      (try
        (Integer/parseInt x)
        (catch Exception e
          invalid))
      invalid)))

(def ->int (s/conformer x-integer?))

(s/def :opt.visits/fromDate ->int)
(s/def :opt.visits/toDate ->int)
(s/def :opt.visits/country string?)
(s/def :opt.visits/toDistance ->int)

(s/def :opt.visits/params
  (s/keys :opt-un [:opt.visits/fromDate
                   :opt.visits/toDate
                   :opt.visits/country
                   :opt.visits/toDistance]))

And here are some usage examples:

(let [spec :opt.visits/params
      data {:some :map :goes :here}]
  (if-let [cleaned-data (validate spec data)]
    ;; cleaned-data has values coerced from strings to integers,
    ;; quite useful for POST parameters
    (positive-logic cleaned-data)
    ;; error values holds a map that describes an error
    (let [error (spec-error spec data)]
      (error-logic-goes-here error))))

What might be improved here is to have a combo-function with both validate and error functionality. Such a function could return a vector of two values: success flag and either result or error data structure as follows:

[true {:foo 42}] ;; good result
[false {:error :map}] ;; bad result

The Spec library does not dictate a single way of processing data; that's why it's really good and flexible.

Sign up to request clarification or add additional context in comments.

1 Comment

Thank you, I will look into this. I think the issue I'm having though is that all the examples online show using spec in the repl and aside from copying and pasting these tests into the repl after entering the namespace I'm working in, I have no idea how to run these tests if they are written in my editor. I am used to Ruby where you just write the tests in a separate rspec doc, require it, then run. If there is any documentation about how to do that in Clojure, would love to see it.
0

valid? is a predicate that returns true or false. Your program isn’t doing anything with the return value. Try printing it to console or using s/assert if you want to throw an exception:

If (check-asserts?) is false at runtime, always returns x. Defaults to value of 'clojure.spec.check-asserts' system property, or false if not set. You can toggle check-asserts? with (check-asserts bool).

So you may need to set (s/check-asserts true) to have s/assert throw exceptions:

(clojure.spec.alpha/assert even? 3)
=> 3
(clojure.spec.alpha/check-asserts?)
=> false
(clojure.spec.alpha/check-asserts true)
=> true
(clojure.spec.alpha/assert even? 3)
ExceptionInfo Spec assertion failed
val: 3 fails predicate: :clojure.spec.alpha/unknown
clojure.core/ex-info (core.clj:4739)

5 Comments

s/assert won't work either and neither does s/conform.
@JessieRichardson "wont" or "doesn't"? conform doesn't work for the same reason as valid?; neither throws. Did you try assert? From the docs: "Returns x if x is valid? according to spec, else throws an ex-info with explain-data plus ::failure of :assertion-failed."
spam-problem.core=> (require '[clojure.spec.alpha :as s]) nil spam-problem.core=> (s/conform even? 1000) 1000 spam-problem.core=> (s/conform even? 1001) :clojure.spec.alpha/invalid spam-problem.core=> (s/assert even? 1000) 1000 spam-problem.core=> (s/assert even? 1001) 1001
In actual editor, no feedback when running lein run. Just: Java HotSpot(TM) 64-Bit Server VM warning: Unable to open cgroup memory limit file /sys/fs/cgroup/memory/memory.limit_in_bytes (No such file or directory)
I'm not sure you have a spec problem. Try just throwing an exception in your main and nothing else: (throw (ex-info "test" {})). I feel like the way you are launching it is a method that does not print out the error out of programs.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.