I use a library called Pathom3. The Getting Started page opens with
Pathom is a Clojure/script library to model attribute relationships.
The README states
Logic engine for attribute processing for Clojure and Clojurescript.
There are a few big ideas in here, but I want to focus on just one - attributes.
What's an attribute?
Before working with Pathom, I'd never really worked with attributes. It's my blog post, so I get to define it:
A pairing of:
an unambiguous global name and
a value
What do attributes look like in Pathom/Clojure?
:com.brettrowberry.posts/display-name ; unambiguous global name
"Pathom: Attributes" ; value
If you don't recognize the :com.brettrowberry.posts/display-name
syntax, here's a primer on Clojure keywords. In this example, it's a namespaced or qualified keyword. The Clojure snippet below demonstrates that namespaced keywords are first class:
(namespace :com.brettrowberry.posts/display-name)
;=> "com.brettrowberry.posts"
(name :com.brettrowberry.posts/display-name)
;=> "display-name"
Notice that we're following the time-honored tradition of using reverse-DNS notation for the namespace part.
What isn't an attribute?
A bare value, e.g.
"Pathom: Attributes"
A name, no matter how unambiguous, e.g.
:name
An ambiguous local name and a value, e.g.
:name "Pathom: Attributes"
:name
could mean any number of things across a variety of contexts, like the name of a blog post, or the name of its author! I consider ambiguous local names to be "normal" programming practice (in Clojure or otherwise), as compared to the more enlightened attribute-driven world. Let's take a look at another thing that isn't an attribute: aggregates.
Aggregates
Aggregates are a composition of values, or key-value pairs (think Clojure maps).
Good Aggregates
(def global-names
{:com.brettrowberry.posts/post-name "Pathom: Attributes"
:com.brettrowberry.posts/author-name "Brett Rowberry"})
(defn greet-attribute
[{:keys [com.brettrowberry.posts/author-name]}]
(str "Hi, " author-name))
(greet-attribute global-names)
This global-names
aggregate is unusually nice in that it's composed of attributes. In greet-attribute
, there's no tricky traversal and no wondering what exactly the input is. Isn't it great?
Bad Aggregates
Generally, the aggregates I've encountered are sets of ambiguous, and often nested, local names and value pairs, e.g.:
(def local-names
{:post {:name "Pathom: Attributes"}
:author {:name "Brett Rowberry"}})
Consider the greet-local
function below. There's no context for what :name
is – the name of a post, of an author, or something else. When aggregates like names
spread across a codebase, it can be hard to know to what :name
refers.
(def local-names
{:post {:name "Pathom: Attributes"}
:author {:name "Brett Rowberry"}})
(defn greet-local
[{:keys [name]}]
(str "Hi, " name))
(greet-local (:author local-names))
An alternative that preserves the context requires traversing the whole structure, as in the greet-traversal
function below:
(def local-names
{:post {:name "Pathom: Attributes"}
:author {:name "Brett Rowberry"}})
(defn greet-traversal
[{{:keys [name]} :author}]
(str "Hi, " name))
(greet-traversal local-names)
It can become tiresome when many functions have to perform the same traversal.
Blast from the F# Past
Sorry, F# was my favorite language before Clojure, so I can't resist a comparison. The closest thing to attributes I encountered before Pathom was single-case Discriminated Unions in F#. Here's the best I could come up with:
namespace Com.Brettrowberry
module Post =
type Name = Name of string
module Author =
type Name = Name of string
Grouping these into an aggregate, an F# Record, isn't great:
type Names = { PostName: Com.Brettrowberry.Post.Name
AuthorName: Com.Brettrowberry.Author.Name }
Identifiers in F# (unlike Clojure keywords) can't have .
in them, so I can't just write something like the right side. So, I made up more local names: PostName
, and AuthorName
.
I could have avoided declaring the Names
type using an Anonymous Record.
SQL
Relational databases struggle with attributes as well. We can make schemas and tables, but that's as deep as the hierarchies go. We can't put .
in schema names, but we can put _
. We could make a single-column table per attribute, but that's pretty unusual.
CREATE SCHEMA com_brettrowberry;
CREATE TABLE com_brettrowberry.post (name text);
CREATE TABLE com_brettrowberry.author (name text);
insert into com_brettrowberry.post values ('Pathom: Attributes');
insert into com_brettrowberry.author values ('Brett Rowberry');
Closing
When it comes to data, I assert that the smallest unit is the attribute. Clojure, with its keywords, is uniquely suited to represent attributes. Doing so can provide us with the most flexible designs. In the words of Rich Hickey:
...design is taking things apart in order to be able to put them back together.