Ep 112: Purify!
► Play EpisodeEach week, we discuss a different topic about Clojure and functional programming.
If you have a question or topic you'd like us to discuss, tweet @clojuredesign, send an email to feedback@clojuredesign.club, or join the #clojuredesign-podcast
channel on the Clojurians Slack.
This week, the topic is: "pure data, pure simplicity". We loop back to our new approach and find more, and less, than we expected!
Our discussion includes:
- Sportify reaches the end zone!
- How can you make software extremely reliable?
- How can you keep logic pure when it depends on the outcome of I/O?
- What is the "think, do, assimilate" pattern?
- What is a "context"? What information should it contain?
- What is an "operation"? What information should it contain?
- Why does "structural sharing" matter?
- When to use multimethods vs case statements.
- The benefits of reducing functions.
- Testing with mocks vs data.
- Why test with real-world data?
- How does minimizing I/O make your code testable, inspectable, and repeatable?
- How to support any process conditional.
- A general way to model any process.
Selected quotes
Let's get some Hammock Time. We're big fans of Hammock Time!
We make a function for each of those: think, do, assimilate. A function to figure out the next thing, a function to do it, and a function to integrate the results back into the context. ("determine-operation", "execute-operation", and "update-context".)
It's not a single point of failure! It's a single point of context.
Where you have a binding in a
let
block, you have a key in a context map. There's a symmetry there.You can make the operation map as big, fat, and thick as you want, so "execute-operation" has 100% of everything it needs for that operation.
The "determine-operation" function can decide anything because it has the full context—the full world at its disposal!
Clojure has structural sharing, so all the information is cheap. We can keep a bunch of references to it in memory. We're not going to run out of memory if we keep information about every step.
The "update-context" is a reducer, so we can make a series of fake results in our test and run through different scenarios using "determine-operation" and "update-context". We're able to test all of our logic in our test cases because we can just pass in different types of data.
Your tests are grounded in reality. They're grounded in what has happened.
We've aggressively minimized the side effects down to the tiniest thing possible!
Data is inert. Mocks are not. Mocks are behavior.
You can just literally copy from the exception and put it in your test. There's no need transform it. It is already useful.
It's very testable. It's very inspectable. It's very repeatable. It creates a really simple overall loop.
You want those I/O implementations so small and dumb that the only way to mess them up is if you're calling the wrong function or you're passing the wrong args. Once it works, it will always work, and you no longer have to test it.
We need to build into context every little bit of information we need to know to make a decision.
Context takes anything that is implicit and makes it 100% explicit, because you can't get data across the boundaries without putting it in the context. You have no option but to put everything in the context, so you know everything that's going on.
We're in this machine, and there's no exit. We're on the freeway, and there's no off-ramp. We're in the infinite loop! How do we know we're done?
How do we know we're done?