Attributes

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:

  1. an unambiguous global name and

  2. 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.

Design, Composition and Performance

Did you find this article valuable?

Support Brett Rowberry by becoming a sponsor. Any amount is appreciated!