Blair Nangle

Book Notes: Elements of Clojure

Book notes from Elements of Clojure by Zach Tellman.

Chapter 1: Names

“Names should be narrow and consistent.”

  • Narrow means that the name cannot represent anything else
  • Consistent means that the name is congruent with the surrounding code and should not be misunderstood by someone familiar with the codebase
  • The textual representation of a name is its sign
  • The thing a name refers to is its referent
  • How a name is used is its sense
  • Narrowness does not equal specificity
  • Describe the purpose of the function, not its implementation
  • Consider a name’s sense when thinking about referential transparency
  • The only way to achieve true consistency is to have a one-to-one relationship between signs and senses
  • Favour synthetic names over natural names to avoid ambiguity
  • Synthetic names allow experts to communicate without ambiguity
  • Novices are forced to learn the lexicon if they want to participate—a monad has no sense to a layperson
  • Natural names allow everyone to reason by analogy—great to for quickly grokking a codebase, bad for ensuring reducing ambiguity
  • Choose accordingly!

Naming Data

  • The relationship between our code and the outside world can be adversarial—we should make invariant checks at the periphery of our code
  • vars provide indirection by hiding the underlying value; function parameters provide indirection by hiding the implementation of the invoking function
  • We don’t need to name every intermediate result when transforming data
  • Consistent code means fewer deep dives to understand a codebase’s core concepts
  • Being able to skim and quickly understand Clojure code is a function of the language’s syntax and use of immutable data structures (as well as an individual’s experience)

“If a function’s name is more self-explanatory than any name you can think of, it should be an anonymous function.”

Idiomatic Clojure names

  • Could be anything: x
  • A sequence of anything xs
  • Arbitrary function: f
  • Sequence of arbitrary functions: fs
  • Arbitrary map: m
  • Sequence of arbitrary maps: ms
  • Self-reference: this
  • Arguments of the same datatype: [a b c & rst]
  • Arbitrary expression: form

Narrowing

  • Maps of more narrowly named data, e.g.: class->students, department->classes->student
  • Tuples of more narrowly named data,. e.g.: tutor+student
  • A sequence of tutor-student tuples could be tutor+students, but this could be conflated with tutor-sequence of student tuples—a synthetic name here can remove ambiguity
  • Clearly document synthetic names!

Naming Functions

  • Our data scope at runtime is any data accessible by our thread
  • Functions can do three things: pull new data into scope, transform data, push data into a different scope
  • One function in every process needs to do all three, but most functions should do only one

“Shared mutable state creates asymmetric scopes.”

  • Functions that cross scope boundaries should have a verb in the name
  • Functions that pull data from another scope should have the returned type in their name
  • Functions that push data into another scope should communicate their side effect

If a function only transforms data, we should avoid verbs wherever possible.

Naming Macros

“There are two kinds of macros: those that we understand syntactically, and those that we understand semantically.”

  • If we are required to understand a macro syntactically, this is a poor form of indirection
  • Macros that include with, def or let in their name should have predictable macroexpanded forms
  • It is difficult for a macro to be self-evident—the macroexpanded form and semantics matter more than the name

Chapter 2: Idioms

Inequalities

  • Favour < and <= for
  • Infix, prefix
  • Left or right associative?