How are clojurians handling control flow on their projects?

That’s a great question! I’m interested in answers here as well.

I use a modified let macro called trylet which goes like this:

(with-metrics
  (trylet
    [valid-input (validate-input input)
     read-data (perform-read valid-input)
     rules-passes (protect-business-rules valid-input read- data)
     transformed-data (transform-data read-data (:opts valid-input))
     saved (save-data transformed-data)]
      ;; Response is created and returned here
      {:result :success}
    (catch Exception e
      (log/error
        (str "Failed in API foo with details: "
          {:valid-input valid-input
           :read-data read-data
           :rules-passes rules-passes
           :transformed-data transformed-data
           :saved saved}))
        ;; Error response is created and returned here
        {:result :error})))

What the trylet macro does is that it makes available the bindings inside the catch as well. Where they’ll be nil if they haven’t been bound yet, because the exception was thrown before reaching them.

So I model the data-flow in the let part, which I’ve found is better then a threading macro, because then each step is named (more readable in future) and you can easily do things like take two prior return to pass to the next step or choose where the return of a prior step must go (like first arity, second, thirds). So it’s the most flexible. It also allows you to model more complex flows, like with conditionals, which is not as well supported with threading.

And if anything throws an exception, it short-circuits (unless I explicitly try/catch it inside the let binding), but in my catch because all the bindings are available, I can see what succeeded what failed and what hadn’t run yet, so it’s very easy after to debug.

Edit: Oh, also I would normally move the validation outside of it, as I don’t consider it a part of the business logic. So really things are setup in two:

(defn api-foo
  [input]
  ;; Validate
  (if-not (valid-input input)
    {:result :invalid-input}
    perform-foo))

Where perform-foo is the above function with the trylet.

That’s because I like my business logic to be able to assume they got valid input. It means I can test them with a generative spec. It’s one less thing to have in them.

In api-foo I would also have all the scaffolding for the particular server I’m hooked up on, so this is a bit of the facade pattern for those familiar with that. So that means in api-foo I would validate and conform and coerce all the input into the exact shape and type my business logic wants, and I would unwrap the business logic result and similarly convert it to whatever response is needed for the given server.

7 Likes