How to build robust data apis in clojure?

Maybe you are looking for records?

(require '[clojure.set :as cset])

(defrecord Person [name age address phone email])

  ;; Make a keyword-based constructor to verify 
  ;; args and decouple ordering.
(let [valid #{:name :age :address :phone :email}]
  (defn mk-person[& args]
    (let [h (apply hash-map args)
          invalid (cset/difference (set (keys h)) valid)]       
      (when-not (empty? invalid)
        (throw (IllegalArgumentException. (pr-str invalid))))
      ; any other argument validation you want here
      (Person. 
        (:name h) (:age h) (:address h) (:phone h) (:email h)))))

=> (def p (mk-person :name "John" :email "[email protected]"))
#:user.Person{:name "John", :age nil, :address nil, :phone nil, 
              :email "[email protected]"}

Now you can choose whether you want exceptions for mistyped names by accessing the data with functions (exception) or keywords (not exception).

=> (.fax p) 
java.lang.IllegalArgumentException: 
    No matching field found: fax for class user.Person
=> (:fax p)
nil

This approach requires that you avoid field names that would conflict with existing methods. (See comment from @Jouni.)

Alternatively, you can bypass the field name limitation by using keywords for lookup and an accessor function that checks for invalid keys:

(defn get-value [k rec]
  (let [v (k rec ::not-found)]
    (if (= v ::not-found)
      (throw (IllegalArgumentException. (pr-str k)))
    v)))

=> (get-value :name p)
"John"
=> (get-value :fax p)
IllegalArgumentException: :fax

"Destructuring the wrong part of the list"-type problems may come from trying to encode something like "person" in a list; then you need to remember stuff like "the zip code is the fourth element in the 'address' list at position three in the 'person' list".

In 'classical' Lisp you might solve that by writing accessor functions, in Clojure you might use records.

Typos will cause problems in any programming language, the best you can do is to try to catch them early.

A Java IDE with autocompletion might catch some typos while you're still typing, and a statically typed language will catch many of them at compile time, but in a dynamic language you won't find them until run time. Some people consider this a drawback of dynamic languages (including Python, Ruby etc.), but given their popularity quite a few programmers think that the flexibility gained and code saved is more important than the loss of IDE autocompletion and compile time errors.

The principle is the same in either case: Earlier exceptions are better, since there is less code to wade through to find the cause. Ideally the stack trace would lead you straight to the typo. In Clojure, records and accessor functions give you that.


Write validators functions for your "schemas" (keys but also type of values etc.) then use thm inside pre- and post- conditions in your code -- since their syntax is little known here is a quick refresher:

(defn foo [x y] ; works with fn too
  {:pre [(number? x) (number? y)]
   :post [(number? %) (pos? %)]}
  (+ (* x x) (* y y)))

They rely on assert and hence can be disabled. (doc assert) for more details.