In Clojure.spec, you spec out namespaced keywords once.
(def email-regex #"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,63}$")
(s/def ::email-type (s/and string? #(re-matches email-regex %)))
(s/def ::acctid int?)
(s/def ::first-name string?)
(s/def ::last-name string?)
(s/def ::email ::email-type)
Using those keywords you can define maps which specify shapes of data:
(s/def ::person (s/keys :req [::first-name ::last-name ::email]
:opt [::phone]))
Functions have their own separate specifications. Here's one that accepts a person and an acctid:
(s/fdef add-to-account
:args (s/cat :person ::person :acctid ::acctid)
And must be called like this:
(add-to-account {::first-name "John" ::last-name "Smith" ::email "john@smith.com"} 12345)
If you tried calling it with an illegal argument and it's instrumented you will see an error:
(add-to-account {::first-name "John" ::last-name "Smith" ::email "abc123"} 12345)
ExceptionInfo Call to #'scratch.core/add-to-account did not conform to spec:
In: [0 :scratch.core/email] val: "abc123" fails spec: :scratch.core/email-type at:
[:args :person :scratch.core/email] predicate: (re-matches email-regex %)
Here's the difference. What if I have another function that just accepts an email:
(s/fdef lookup-user
:args (s/cat :email ::email)
And another which looks up by last name:
(s/fdef lookup-user-by-name
:args (s/cat :last-name ::last-name)
And now imagine if you wanted to accept either an email or last-name (notice it does not match our person spec). Attempting to use interfaces would quickly get out of control. You'd have to create an interface for every single property and extend interfaces to form arbitrary groups of properties.