Sveriges mest populära poddar

Functional Design in Clojure

Ep 026: One Call to Rule Them All

30 min • 26 april 2019

Christoph 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.
  • 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.
  • 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:

Clojure in this episode:

  • defprotocol
  • defrecord

Related links:

Förekommer på
00:00 -00:00