How to Clojure.Spec a reference type (like atom)?
Asked Answered
C

3

5

I wonder how I would spec a function, that has a parameter, that hold a map in an atom.

(defn do-something [a]
  (prn (vals @a)))

This those obviously not work:

(s/fdef do-something
  :args (s/cat :a map?))

How would I spec that a is a reference to map?

Centralia answered 22/6, 2016 at 15:14 Comment(0)
L
11

Don't. clojure.spec is about specifying the structure of data and atoms are state, not data. Not every function necessarily needs to have (or check) a spec.

My general advice for dealing with stateful data is to:

  • Define your data
  • Define pure functions to that take and return your data
  • Create specs for those data functions
  • Manipulate atoms using only those pure functions in as few places as possible

With some care, you can often reduce the number of functions that take or return atoms to 0 (by closing over the atom in the place where it's managed), which is imho a worthy goal.

Limburger answered 22/6, 2016 at 15:41 Comment(3)
How about specing the atom (/ref/agent) itself for constraints? (and if that were possible -- as I think it should be -- it would also make sense to specify those few functions that take or return an atom).Serdab
@Serdab I think the suggestion from Timothy in the other answer via validators is the best way to spec a reference type itself. I think it's fine to spec a function as taking an #(instance? clojure.lang.IAtom %) or whatever. But not every function necessarily needs a spec.Limburger
Right, I forgot about validators. It would be nice to make it more structured and idiomatic, though. I absolutely agree that not everything needs a spec, but it's good to have full spec expressiveness if you want (in the future there may be more tools, like static analysis, that could benefit from that).Serdab
J
8

You don't. For one reason, it wouldn't be thread-safe. If you did somehow spec that a atom contained a map, it could change to an integer in the time it takes you to check the atom, and continue with your function.

One option, however, is to provide a validator to the atom. You could easily use partial to do this: (set-validator! my-atom (partial s/valid? :my-spec)). Now the atom will fail to update unless the value conforms to :my-spec.

Another option is to add validation logic to all the functions that update the atom. Which of these two approaches works best depends on the application.

Judaist answered 22/6, 2016 at 15:37 Comment(1)
Engineering best practices for untyped languages typically frown on assigning different types to a variable. Such a practice often leads to bugs in a system, hurts readability, and negatively impacts performance.Intransigent
I
1

You can use with-gen, a custom predicate, and a custom generator:

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

(defn memoize! [memo key distance]
  "Memoizes the distance at the given key and returns the distance"
  (swap! memo assoc key distance)
  distance)

(spec/def ::word
  (spec/and string? (complement nil?)))

(defn map-atom? [o]
  (and (instance? clojure.lang.IAtom o)
       (map? @o)))

(def map-atom-gen
  (gen/fmap
   (fn [_] (atom {}))
   (gen/int)))

(spec/def ::map-atom
  (spec/with-gen map-atom?
    (constantly map-atom-gen)))

(spec/fdef memoize!
           :args (spec/tuple ::map-atom
                             (spec/coll-of ::word :type vector? :count 2)
                             nat-int?)
           :ret nat-int?
           :fn (fn [{[memo key distance] :args, retval :ret}]
                 (= distance (@memo key) retval)))
Intransigent answered 28/12, 2017 at 22:27 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.