on the clojure move

jump to main content

Naming code, the value-identity relation

There are only two hard things in Computer Science: cache invalidation and naming things.

[source: famous programming joke attributed to Phil Karlton (a Netscape engineer)]


[/Names]

… Our name is now layer of indirection, separating what the function does from how it does it….

Indirection, also sometimes called abstraction, is the foundation of the software we write. Layers of indirection can be peeled away incrementally, allowing to us to work within a codebase without understanding its entirety. Without indirection, we’d be unable to write software longer than a few hundred lines.

Names are not the only means of creating indirection, but they are the most common. The act of writing software is the act of naming, repeated over and over again. It’s likely that software engineers create more names than any other profession. Given this, it’s curious how little time is spent discussing names in a typical computer science education. Even in books that focus on practical software engineering, names are seldom mentioned, if at all….

[/Names/Naming data]

Finding good names is difficult, and so wherever possible we should avoid trying.

[source: https://elementsofclojure.com/ by by Zachary Tellman]


clojure.core/def

special form

Usage: (def symbol doc-string? init?)

Creates and interns a global var with the name of symbol in the current namespace (ns) or locates such a var if it already exists. If init is supplied, it is evaluated, and the root binding of the var is set to the resulting value. If init is not supplied, the root binding of the var is unaffected.

Please see https://clojure.org/reference/special_forms#def

[source: https://clojure.github.io/clojure/clojure.core-api.html#clojure.core/def]


1. Naming code as the relation of identity and value

It seems that clojure.core/def is exactly about this relation, and clojure requires a symbol value as a identity. Once the relation is internalised (evaluated), the identity/symbol will be replaced by the value of that naming relation.

So, that's clear, and also, that identity choice (namespaced symbol) is semantically "place oriented": 'folder-path-and-file/symbol-name.

But, identities don't need to be "an always one and only one value replacement relation". An example of that is clojure.spec/def

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

(s/def ::foo string?)
;;=> ::foo

(eval ::foo)
;; => ::foo

clojure.spec/def is not only different from clojure/def for that, clojure.spec chooses namespaced-keyword as identity value, but clojure/def chooses symbol for the same purpose.

There are tons of good consequences from using namespaced-keywords that I've already talk about it on "Decomplecting clojure.spec: From validation to self-discoverable semantic services", thus I'll not get into that now. In this post I'd like to get into the 1-1 identity-value relation.

2. why "always one and only one" identity-value relation?

why not a collection of identities to mean/fetch a place/value? If "Finding good names is difficult, and so wherever possible we should avoid trying", why not using a combination of generic identities instead of trying a condensed and ambiguous one only identity?

 (def com.app.endpoint.request.post.payload.user/profile ...)
;; or
  (def com.app.user.profile.endpoint.request.post/payload ...)

;; vs
 (def #{com.app/endpoint request.post/payload user/profile} ...)

Of course with clojure.core/def we can't use a set of keywords to define the identity part on the act of naming, but, clojure.spec/def already showed that we can do variations on the same def theme. Eg: (s/def ::foo ....)

3. clojure.spec/keys with semantic/compound-identity

clojure.spec introduced with spec/keys an unintended "naming (Maybe Not) problem". The consequence of that, we need to define endless types to cover all the different contexts where this identity needs to exist.

Let's try a compound identity for this set of keys instead. So instead of naming a collection of clojure.spec/keys with 1 namespace-keyword (s/def ::xxx (s/keys [....])", let's use a compound-identity with just the collection of spec-keys (semantic-namespace.spec.keys/def #{::xxx ::yyy} [....])

Example using semantic-namespace/spec.keys

(require '[semantic-namespace.spec.keys :as spec.keys]
  '[clojure.spec.alpha :as s])

(s/def ::id string?)
(s/def ::alias string?)

(spec.keys/def
  #{:app.api.endpoint.get/response
    :app.domain/user}
  [::id ::alias])
;;
(spec.keys/valid?
 #{:app.api.endpoint.get/response
   :app.domain/user}
 {::id "foo" ::alias "@foo"})
;;=> true

(spec.keys/valid?
 #{:app.api.endpoint.get/response
   :app.domain/user}
 {::id "foo" ::alias 55})
;;=> false


(spec.keys/valid?
 #{:app.api.endpoint.get/response
   :app.domain/user}
 {::id "foo" ::emal "foo@foo"})
;;=> false

This tiny example shows that we can define contexts via identity on data expectations, but also that we can avoid the naming complexity problem using generic identities.

Hope you enjoyed the topic, I'd love to hear your thoughts https://news.ycombinator.com/item?id=45637111