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-studenttuples could betutor+students, but this could be conflated withtutor-sequence ofstudenttuples—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,deforletin 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?