Let me add some details to my answer to make it more useful.
What I generally do is I’ll use Spec (but you can substitute with whatever else)
;;;;;;;;;;;;;
;;;; Car ;;;;
(s/def :car/name string?)
(s/def :car/model #{:subaru :ford :nissan})
(s/def :car/car
(s/keys :req-un
[:car/name :car/model]))
That’s like my “class”, except it’s not a class at all, just a generic description of the shape of a map that represents a logical Car in my application.
Then I’ll add a constructor function which I normally add right under:
(defn make-car
([car] (s/assert :car/car car))
([name model]
(make-car
{:name name
:model model})))
So all together now you have:
;;;;;;;;;;;;;
;;;; Car ;;;;
(s/def :car/name string?)
(s/def :car/model #{:subaru :ford :nissan})
(s/def :car/car
(s/keys :req-un
[:car/name :car/model]))
(defn make-car
([car] (s/assert :car/car car))
([name model]
(make-car
{:name name
:model model})))
;;;; Car ;;;;
;;;;;;;;;;;;;
Now anywhere in my app where I use a car
I’ll call the variable or parameter for it car
and if I destructure it I’ll do: {:keys [model] :as _car}
to indicate logically I’d expect a car to be passed where I only use the :model
key from it.
And when I need to update a car
I’ll just add a call to make-car
after I’ve made my changes to it, and the doc-string will mention I return a car
.
(defn change-name
"Takes a car and a new-name for it, and returns a car
whose :name is the new-name."
[car new-name]
(-> car (assoc :name new-name) (make-car)))
Generally I find that’s enough, just by using the entity names only when I expect the value to be valid to the spec of that entity I’ve found it’s enough. You see anywhere something called car
and you know it’s supposed to be of that spec.
When something doesn’t refer to a top-level entity, like take :car/model
, maybe I also have :truck/model
then I don’t call the variable/parameter model
but instead I call it truck-model
.
My top level entities are normally specced as :car/car
, :truck/truck
.
If there’s some deeper hierarchy, like say each model are a map themselves, and they too conflict, which is pretty rare, but it happens.
Say a user has a credit-card that has a number which is the credit-card number. But in another context a bank has a credit card that has a number but that number refers to like the type of cardSapphire gold, Sapphire silver, and not the actual credit card number.
Like:
;; User
{:user-id 123
:credit-card {:number "7467-7364-8283-2234"}}
;; Bank
{:bank-name "Chase"}
:credit-card {:number 482684}}
I’ll use the following name:
(s/def :bank.credit-card/number ...)
(s/def user.credit-card/number ...)
And in my code will call these: user-credit-card-number
and bank-credit-card-number
.
It’s kind of rare though that a function takes that directly. I wouldn’t name locals like that, because with locals the context is clear:
(defn do-wtv
[bank]
(let [number (-> bank :credit-card :number)]
...)
And keep in mind often those are not hierarchical, like if I use credit-card everywhere I’d have:
(s/def :credit-card/credit-card
(s/keys :req-un
[:credit-card/name :credit-card/number :credit-card/expiry]))
(s/def :bank/bank
(s/keys :req-un
[:credit-card/credit-card]))
It’s only if under some entity you’ve got something called credit-card that is logically a different kind of credit-card specific only too that entity that I’d give it a hierarchical name like bank/credit-card
or bank.credit-card/number
.
And if you need derived entities, I use multi-spec and a :type
key on my maps.
And when I make a backward breaking change I also turn it into a multi-spec and add a :version
key.
Finally, at the app boundary, so wherever you do I/O, I explicitly call s/valid?
to make sure I’m producing or receiving a valid entity.
Then in my code, generally just this naming convention is good enough, but like I showed I sprinkle some make-car
whenever I return an updated car
and that will run assert on it, and I might add a few s/fdef
and fn instrumentation as well in some places if I feel the need to be sure my function is called with an actual car
. The other benefit is I can then easily add a generative test for those functions. Both asserts and instrumentation would only be turned on in dev/test/staging only so as not to incur their runtime cost.