@DanLebrero.

software, simply

Book notes: Elements of Clojure

Book notes on "Elements of Clojure"

These are my notes on Zachary Tellman’s Elements of Clojure.

There are some elements of Clojure on the book, but most of the content covered is universal to any language.

Zach has several talks were he covers the subjects of the book: names, abstraction and composition.

Key Insights

  • Good software is built through effective indirection.
  • Names are the most common way of creating indirection.
  • The act of writing software is the act of naming repeated over and over again.
  • Any project involving multiple people exists in a continuous state of low-level confusion.
  • We are constantly drawn to software we don’t know well enough to dislike.
  • The models of SW are build atop inductive analogies and aspire only to satisfice.
  • Broad assumptions means smaller models which means simpler code.
    • Over-engineered code == too few assumptions.
  • If a model cant fit in our head, it has little value.
  • Build SW from principled components (build to be discarded), separated by interfaces where necessary (build to last).
  • City planning is a better metaphor for SW development than civil engineering.
  • The ultimate goal of composition is to define processes.
  • Any system that exceeds our understanding will inevitably grow a bit slow and flaky.

TOC

Introduction

  • To write software we must learn to draw boundaries.
  • Good software is built through effective indirection.
  • Finding good names is difficult, so we should try to avoid.
  • Any project involving multiple people exists in a continuous state of low-level confusion.

Names

  • Names are the most common way of creating indirection.
  • The act of writing software is the act of naming repeated over and over again.
  • Names are composed of:
In SW terms
Sign Textual representation Textual representation
Referent What it references Current implementation
Sense How it is referenced Set of fundamental properties we ascribe to it
  • When finding a new name, we only need to understand its sense.
  • Sense can be communicated through:
    • Sign.
    • Context.
    • Documentation.
    • Everyday conversation.
  • Names should be:
    • Narrow:
      • Exclude things it cannot represent.
      • Reveals its sense:
        • Too specific leaks implementation details.
        • Too general misses fundamental properties, inviting breaking changes.
    • Consistent: easily understood in its context.
  • Any project involving multiple people exists in a continuous state of low-level confusion.
Natural Names Synthetic Names
Multiple senses One sense
Ambiguous Fully consistent with sign
Reason by analogy Sense must be learn
Lower entry barrier High entry barrier
  • The threshold for self-evidency depends on the reader.
  • Finding good names is difficult, so we should try to avoid.
  • Functions can do three things:
    • Pull data into scope: Name should describe the data type it returns.
    • Transform data in scope: Avoid verbs.
    • Push data to another scope: Name should be the effect it has.
  • At least one function in every process must do all three. This functions are difficult to reuse.
  • All functions in a namespace should operate on a common data type and/or data scope.
  • As few namespaces as possible.
  • Macros are poor means of indirection.

Idioms

  • Prefer < or <=. Always use the same.
  • Avoid named parameters.
  • Only use letfn when mutual recursion is required.
  • Use for for cartesian products.
  • Check for nil at regular intervals.

Indirection

  • Two indirection tools:
    • References:
      • Conveys values.
      • Open.
    • Conditionals:
      • Decides based upon values.
      • Closed: to change them, we need to change code.
  • nil has been know to cause runtime errors.
  • For a decision mechanism to be open it must be unordered: table with unique keys.
  • Indirection is a mechanism for creating abstractions.
  • Indirection allow us to work with a codebase without completely understand it.
  • Proofs lack context: they are only concern with being self-consistent.
  • In SW, we don’t have the luxury of ignoring context. Our models must be self-consistent and useful within our context.
  • Self-consistency is objective, usefulness is subjective.
  • Model for modules:
    • Model: data and functions.
    • Interface: means by which model and environment interact.
    • Environment: everything else.
    • Assumption: everything that the model does not represent.
    • Invariants: to avoid model’s invalid states.
    • Invalid states: those which cannot be found in the environment.

Module model

  • Better models: those with less exceptions.
  • Reasoning:
    • Deductive:
      • Conclusions are necessary: if they are wrong is because the initial assumptions were wrong.
      • Tries to predict.
    • Inductive:
      • By analogy, only compares.
      • Conclusions are contingent: they are allowed to be wrong.
  • Inductive is more resilient than deductive at the cost of not being optimal.
  • Model satisfices if it is good enough given the environment.
  • The models of SW are build atop inductive analogies and aspire only to satisfice.
  • Broad assumptions means smaller models which means simpler code.
  • Group models with similar assumptions, wrap them in a single layer that enforces those assumptions.
  • Abstractions that fail together should stay together.
  • Better modules == more useful.
  • Models reflect our perception of their environment.
    • There is no objective measure of the importance of a given facet.
  • A module is useful only if its assumptions are sound now and in the near future.
  • Over-engineered code == too few assumptions.
  • Every conversation about SW can be more productive by describing, up front, our subjective understanding of its environment.
  • We are constantly drawn to software we don’t know well enough to dislike.
  • Confidence requires understanding: I disagree! Unconscious incompetence
  • A module cannot prevent itself to be misused.
  • Convention: how we enforce assumptions that are not hidden away by an abstraction layer (because abstraction layer is too expensive to build or execute)
    • Are a useful tool but not a solution.
  • The “seniority” of an engineer derives more from their ability to predict adverse environments than from mastery of any particular technology.
  • The easiest way to know that a failure mode exists is to see it happen.
  • If a model cant fit in our head, it has little value.
    • As growing a model is not a problem if our understanding grows with it. Still a problem for new people.
    • As we internalize a model, individual faces coalesce into larger, more manageable concepts.
  • If we dont solve a user’s problem, someone else will.
  • City planning is a better metaphor for SW development than civil engineering.
  • SW would be easy if thing never changed.
  • Build SW from principled components (build to be discarded), separated by interfaces where necessary (build to last).
  • Principled systems:
    • Minimal indirection.
    • Hierarchical: each layer smaller, faster and with broader assumptions.
    • Fragile.
    • Easier to understand (per layer).
  • Adaptable system:
    • Loads of indirection.
    • Larger component, less efficient, redundant.
    • More flexible.
  • Clojure promote the creation of adaptable software.

Composition

  • The ultimate goal of composition is to define processes.
  • Process always:
    • Pull data from environment.
    • Transform data.
    • Push data to environment.
    • Keep them separated until the last possible moment.
  • Communication between processes is only possible via shared references.
  • (side) effect: any change to shared references.
  • We can consider a process in isolation if:
    • Performance isnt a primary concern.
    • Some (timeout) failures are acceptable.
  • Any system that exceeds our understanding will inevitably grow a bit slow and flaky.
  • Execution model: strategies describing what our process will do when its environment provides too much or too little.
  • Push and pull phases:
    • Enforce invariants.
    • Context dependant.
  • Transform:
    • Functional.
    • Either accrete, reduce or reshape.
    • Return a descriptor of the effects to be performed.
  • Keep transform code in a different namespace than push/pull code.
  • In a robust process, pull phase invokes transform phase so handling errors is a pull phase concern.
    • Using lazy-seqs with side effects breaks this rule.

Did you enjoy it? or share!

Tagged in : Clojure book notes