开发者

How to build robust data apis in clojure?

I find that my clojure apps get structurally coupled very rapidly due to the lack of a data API. I have maps with keys that have names which, if mistyped, because exceptions to be thrown or 开发者_如何转开发errors. I also notice that it's easy to make mistakes when destructuring a list (for example maybe you destructure the wrong part of the list).

Coming from the Java world, normally I use my IDE to help me to get the "right" data out of minimal, unordered data objects. But clojure map passing seems to be the opposite paradigm from this.

How do clojurians code defensively in the absence of a type system or ide code completion?


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.


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 "john@hotmail.com"))
#:user.Person{:name "John", :age nil, :address nil, :phone nil, 
              :email "john@hotmail.com"}

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.

0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜