Clojure: Ensure data integrity when creating a Rec

2020-03-30 06:46发布

I'm learning Clojure and enjoying it but find an inconsistency in Records that puzzles me: why doesn't the default map constructor (map->Whatever) check for data integrity when creating a new Record? For instance:

user=> (defrecord Person [first-name last-name])
#<Class@46ffda99 user.Person>
user=> (map->Person {:first-name "Rich" :last-name "Hickey"})
#user.Person {:first-name "Rich" :last-name "Hickey"}
user=> (map->Person {:first-game "Rich" :last-name "Hickey"})
#user.Person {:first-game "Rich" :first-name nil :last-name "Hickey"}

I believe the Map is not required to define all the fields in the Record definition and it is also allowed to contain extra fields that aren't part of the Record definition. Also I understand that I can define my own constructor which wraps the default constructor and I think a :post condition can then be used to check for correct (and comprehensive) Record creation (have not been successful in getting that to work).

My question is: Is there an idiomatic Clojure way to verify data during Record construction from a Map? And, is there something that I'm missing here about Records?

Thank you.

标签: clojure
2条回答
够拽才男人
2楼-- · 2020-03-30 07:19

I think your comprehensiveness requirement is already quite specific, so nothing built-in I know of covers this.

One thing you can do nowadays is use clojure.spec to provide an s/fdef for your constructor function (and then instrument it).

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

(defrecord Person [first-name last-name])

(s/fdef map->Person
  :args (s/cat :map (s/keys :req-un [::first-name ::last-name])))

(stest/instrument `map->Person)

(map->Person {:first-name "Rich", :last-name "Hickey"})
(map->Person {:first-game "Rich", :last-name "Hickey"})  ; now fails

(If specs are defined for ::first-name and ::last-name those will be checked as well.)

查看更多
欢心
3楼-- · 2020-03-30 07:26

Another option is to use Plumatic Schema to create a wrapper "constructor" function specifying the allowed keys. For example:

(def FooBar {(s/required-key :foo) s/Str (s/required-key :bar) s/Keyword})

(s/validate FooBar {:foo "f" :bar :b})
;; {:foo "f" :bar :b}

(s/validate FooBar {:foo :f})
;; RuntimeException: Value does not match schema:
;;  {:foo (not (instance? java.lang.String :f)),
;;   :bar missing-required-key}

The first line defines a schema that accepts only maps like:

{ :foo "hello"  :bar :some-kw }

You wrapper constructor would look something like:

(def NameMap {(s/required-key :first-name) s/Str (s/required-key :last-name) s/Str})

(s/defn safe->person 
  [name-map :- NameMap]
  (map->Person name-map))

or

(s/defn safe->person-2
  [name-map]
  (assert (= #{:first-name :last-name} (set (keys name-map))))
  (map->Person name-map))
查看更多
登录 后发表回答