Grokking Simplicity, part 1
I was recently reminded of Manning’s Grokking series in a discussion on Hacker News. So I figured I’d pick up a copy of Grokking Simplicity, by Eric Normand. The name frustratingly hides the fact that it’s about Functional Programming. I took a bunch of notes while reading it, and I figured I’d clean them up a bit and publish them as a blog post.
This was a pretty interesting book overall. I’ve been programming for over 25 years, mostly in Imperative and Object-oriented styles. I’ve used a bunch of Functional Programming techniques over the years, moreso when Java started to embrace them, but the book still had plenty to teach. At a little over 500 pages, it’s a thorough introduction, with excellent explanations and a gradual introduction of new concepts. This is not a review, but my one point of criticism is that the final chapters (about Functional Architectures) are rather underdeveloped compared to the earlier chapters. These chapters really should be part of a more advanced book on Functional Architecture, and don’t belong in an introductory text like this.
As my notes on this book turned out to be rather extensive (some 3000 words unedited), I’ll split this into multiple blog posts. Consider this to be Part 1, which deals with Stratified Design. There will be a Part 2 which covers Abstractions.
Data vs Calculations vs Actions
Early on, the book defined three categories of “things”: Data, Calculations, and Actions. It was nice to see this defined explicitly; it’s one of those things that seems self-evident but doesn’t get the attention it probably deserves. The idea is simple. Data is “inert”, it doesn’t do anything, it just is. Calculations are “pure functions”, they have no side-effects, they are repeatable, their results depend entirely on their inputs. Actions are the hard stuff: they can have side effects, they can interact with the outside world, their outcome can depend on when they are executed or what the state of the system is. As such, Data is easier to work with than Calculations, and Calculations are easier than Actions.
A major design goal is to isolate the hard stuff from the easy stuff. This makes it easier to manage.
Inputs & outputs
Inputs & outputs are important for figuring out what can or cannot be turned into a Calculation, and what is an Action. Anything that relies on implicit inputs or outputs is automatically an Action, as implicit in- and outputs are a form of side effects (or are not free from side effects).
Explicit inputs & outputs
- Input arguments
- Return values
Implicit inputs & outputs
- Basically anything that isn’t explicit
- Global, shared state
- Time
- Randomness
Stratified Design
Stratified design refers to “layers” (strata) of abstraction, similar to how in the OSI Model each layer abstracts away the complexities of the previous layer. The Application Layer doesn’t have to care about the Physical medium over which its data is transmitted. The same should be true here.
Each new stratum uses the functions from the lower layers to provide a higher level of abstraction. The higher up we go, the more frequent things change, e.g. the underlying language is pretty stable, but business logic changes all the time. In between we might have a bunch of application-specific primitives which are pretty stable but not as stable as the language. These layers can be numerous, and are not formally named. They are created ad hoc based on what makes sense in any given application.
When higher strata “skip” intermediate strata to directly use, e.g., low-level language constructs, that might be a code smell. Stratification is not about hiding messy complexity in helper functions, each stratum should have a clear purpose.
Wait, layers?
Stratified Design is sort of orthogonal to the traditional Layered Architecture. Layered Architecture is all about separating concerns and dependencies. The UI layer deals with presentation, the Data layer interacts with the database, the Business layer handles the business logic. The layers don’t really depend on one another except for a small interface. But each of these layers could (and probably should?) be stratified. The two models can coexist.
Concrete example of strata
The book’s examples are centered around a shopping cart; it gives the following example for strata:
- Javascript language features
- Copy-on-write operations
- Basic item operations
- Basic cart operations
- General business rules
- Shopping cart business rules
Where higher numbers refer to higher layers of abstraction. It is clear that you don’t want to bother with implementing copy-on-write functionality within the code for a shopping cart, and it makes sense that a shopping cart would make use of high level concepts such as Items or Discounts etc.
Patterns of Stratified Design
- Straightforward implementation. Function bodies should be easy to read, without either drowning in too much detail, nor hiding away the important bits. Aim for the goldilocks zone of detail
- Abstraction barrier: strata can help hide implementation details, helping us free mental capacity to focus on the problem. This also makes it possible for some people to work on generally useful low-level functions, while others use high level code to solve business problems.
- Minimal interface: business logic should be implemented using a small but powerful set of operations.
- Comfortable layers: there should be no strata for the sake of stratification, abstractions should be comfortable to work with, and should be aimed at being productive. There is a tension between “perfect” abstraction, and code that is comfortable to use. When the code is easy to work with: stop abstracting.
Stratification in OOP
While the book deals with Functional Programming, Stratification can just as easily be applied to Object-oriented Programming. Generally speaking, every class will live in one stratum, grouping related functions from that stratum into a cohesive whole. It wouldn’t make sense for a ShoppingCart class to have a readFile() method, but it might make sense for it to have an applyDiscount() method.
Something that I’ve certainly been guilty of is creating a class which has different methods corresponding to different strata. This is better than having all of the stuff in a single method, but still makes the code harder to test, and adds cognitive load.
To be continued
The book offers some advice for how to defined good abstractions. I’ll try to dive into some of those in Part 2. If you find this at all interesting, you might want to consider getting the book.
— Elric