Ep 026: One Call to Rule Them All
► Play EpisodeChristoph thinks goals are data, not function names.
- We were talking about the twitter handle again.
- Last week, we talked about faking. It's not mocking.
- The magic that makes it possible is using a protocol.
- Switch out the real handle or the fake handle in component based on configuration.
- "Yes, I do want to speak to the log file."
- "Sometimes, the log file gets lonely."
- (02:43) Christoph wants to talk about the protocol.
- We made a function for each operation we did in Twitter.
- Post tweet
- Search
- Get timeline
- Each function took the information needed for that operation as individual parameters.
- (04:32) Example: the AWS API wrapper that Cognitect released.
- Other wrappers are enormous, with functions for each AWS functions.
- Cognitect's wrapper has just one:
invoke
. At least only one that gets work done. - Unconventional (aka "weird").
- Take a step back. What needs to happen to call a remote API?
- We need to make an HTTP call to some end point.
- "100% of the information that that server needs to get from us, we transmit as data."
- "The path, the headers, everything about it is data."
- "The function is in the URL and the URL is in the data. It's all data."
- If you want the operations to be exposed as functions, you end up promoting the operation to be a function name, but when making the request you need to put the operation back into the data.
- Benefit (not really): we get to write a lot of boilerplate code.
- "Nothing like some boilerplate to get you warmed up in the morning."
- (09:15) Back to the Twitter handle, what if we just had one function?
- "Making a function for each endpoint goes against what we talked about last week, which is making our context explicit."
- (09:55) First benefit of one function: Simplifies the worker half of our algorithm.
- Before:
- Decider creates map with
{:command :twitter/fetch-timeline :twitter/last-tweet-id 1234}
- Worker uses a multi-method to convert that to
(handle/fetch-timeline handle {:twitter/last-tweet-id 1234})
- Handle function will construct Twitter request
{:url "https://twitter.com/fetch-timeline" :last-tweet 1234}
- Handle sends request to Twitter and handles the response.
- Decider creates map with
- After:
- Decider creates map with
{:command :twitter/operation :twitter/command :fetch-timeline :twitter/last-tweet-id 1234}
- Worker's multi-method detects a Twitter operation and passes the entire operation map to the handle.
- Handle transforms the operation into the data needed for the request.
- Handle sends request to Twitter and handles the response.
- Decider creates map with
- When the data is in the map, it's easier to test.
- (18:03) Second benefit of one function: a place for common code in the handle.
- Inside the handle, you can use a multi-method.
- If there is common code, like auth checking or data transformation, that can go into the single function.
- The same tick-tock that was used to get separate side effects from logic in our algorithm can be used inside the handle.
- "Imperative logic is like a branching tree of side-effects."
- The handle works on a rich map of information, and with one function, we're handing it one rich map of information.
- "You have deciders all the way down."
- (20:52) How do we do spec this single function?
- One function needs to take data in multiple shapes. Making a spec that matches all of those shapes will be difficult.
- This function call other, more specific functions to get its work done, so there can be specific specs on those.
- "You don't have to spec everything at all the levels."
- We can also lightly check the entry point to check for attributes that are common to all shapes.
- "Spec is an open system. I can check for the presence of what I need without having to assert a closed world."
- (22:55) Thinking about the symmetry. The following are equivalent:
- 4 functions that each take 5 parameters.
- 4 functions that each take a map with 5 keys.
- 1 function that takes a map with 6 keys (the last one being the operation).
- Programming systems that are strongly typed push you toward more functions and less data.
- Trampoline from data to function to data to function to data...
- Clojure's structural checking makes it so that we can have assurances about data without being forced to check or name everything.
- "Say what you mean, don't make me read your body language and guess."
Message Queue discussion:
- (25:50) Separating logic from side effects.
- "The tree of side effects is like a bowl where you swirl around into the bottom and then swirl back up to the top."
- In a call stack, side-effects should be shallow on the stack.
- "Pure things can't screw you over."
- "Keep your friends close and your side-effects closer."
- Running this concept out to its logical conclusion enabled us to discover the tick-tock approach.
Related episodes:
- 006: All Wrapped Up in Twitter
- 021: Mutate the Internet
- 022: Evidence of Attempted Posting
- 023: Poster Child
- 024: You Are Here, But Why?
- 025: Fake Results, Real Speed
Clojure in this episode:
defprotocol
defrecord
Related links: