(fn some-function [args]
(let [binding (transform args)]
(an-expression binding)))
I find that this makes skimming lisp code much easier, because I can usually skip reading the bindings and just read the function name and ultimate expression and usually get the gist very quickly. (defrecord UserLoginView [email password])
;; DIFFERENCE: compile-time validation
(defn login [^UserLoginView view]
(authenticate (:email view) (:password view)))
;; Usage
(let [user-data {:user/email "[email protected]"
:user/password-hash "hash123"
:user/address "123 Main St"
:user/purchase-history []}
;; DIFFERENCE: construct the intermediary data structure - ignore extra stuff explicitly
login-view (->UserLoginView
(:user/email user-data)
(:user/password-hash user-data))]
(login login-view))
I prefer not to work this way though. The spec-driven alternative could be: (require '[clojure.spec.alpha :as s])
(require '[clojure.spec.gen.alpha :as gen])
(s/def :user.login/email string?)
(s/def :user.login/password-hash string?)
(s/def :user.login/credentials
(s/keys :req [:user.login/email ;; spec's compose
:user.login/password-hash]))
(defn login [credentials]
;; DIFFERENCE: runtime validation
{:pre [(s/valid? :user.login/credentials credentials)]}
(authenticate (:user.login/email credentials)
(:user.login/password-hash credentials)))
(let [user-data {:user.login/email "[email protected]"
:user.login/password-hash "hash123"
:user/address "123 Main St"
:user/purchase-history []}]
;; DIFFERENCE: extra data ignored implicitly
(login user-data))
;; Can also pass a minimal map
(login {:user.login/email "[email protected]"
:user.login/password-hash "hash123"})
;; or you can generate the data (only possible because spec is a runtime construct)
(let [user-data
(gen/generate (s/gen :user.login/credentials)) ; evaluates to #:user.login{:email "cWC1t3", :password-hash "Ok85cHMP5Bhrd4Lzx"}
]
(login user-data))
The drawbacks of Records are the same for Objects - Records couple data structure to behavior (they're Java classes with methods), while spec separates validation from data, giving you: You say "it makes sense to unit test logins using every conceivable variation of `UserLoginView`", well, with spec you can actually *do that*:
(require '[clojure.test.check.properties :as prop])
(require '[clojure.test.check.clojure-test :refer [defspec]])
(defspec login-always-returns-session 100
(prop/for-all [creds (s/gen :user.login/credentials)]
(let [result (login creds)]
(s/valid? :user.session/token result))))
This is impossible with Records/Objects - you can't generate arbitrary Record instances without custom generators.
- function instrumentation (with clojure.spec.test.alpha/instrument)