How to try/catch Let bindings?

1,842 views
Skip to first unread message

Didier

unread,
Sep 30, 2017, 6:14:33 PM9/30/17
to Clojure
I'm curious how others handle this use case, which I feel should be pretty common.

Given you have a series of business process steps, where the flow is too complex for the arrow macros, and you also like to name the step results descriptively, so you use let:

(let [a (do-a ...)
      b (do-b . a . .)
      c (do-c . a . b)]
  (log/info "Success" {:a a
                       :b b
                       :c c})
  c)

Now, say each steps could possibly throw an exception. So you want to try/catch the let bindings:

(try
  (let [a (do-a ...)
        b (do-b . a . .)
        c (do-c . a . b)]
    (log/info "Success" {:a a
                         :b b
                         :c c})
    c)
  (catch Exception e
    (log/error "Failed" {:a a
                         :b b
                         :c c})
    (throw e)))

But, inside the catch, you need access to the let bindings a,b,c, in order to recover from the failure, or like in my example, just to log so that debugging of the issue is easier.

Now the catch will not be in scope of the bindings, and this won't even compile: "Can not find symbol a,b,c in context...".

What would be the idomatic thing to do here?

Is there another way to execute a set of complex steps which does not rely on let and can be try/catched in the manner I describe?

Or is there a way to re-write the let that satisfies my case, and remains idiomatic, and does not add accidental complexities?

Thank You.

Marcus Magnusson

unread,
Sep 30, 2017, 7:42:48 PM9/30/17
to Clojure
I've used try-let (link below) for this, it's worked great!

https://github.com/rufoa/try-let

Didier

unread,
Oct 1, 2017, 9:39:27 PM10/1/17
to Clojure
I've seen this, I was still curious if the reason I was facing the issue was that let is simply the wrong tool for my use case or not.

If let is the correct tool, I would propose that clojure.core should had a try/catch where the catch is in scope of the try. I feel the reason this is contrived is accidental complexity due to limitations of Clojure. In Java, this isn't an issue, because you're variables would be mutable:


Object a;
Object b;
Object c;
try {
  a
= doA(..);
  b
= doB(.., a, .., .., ..);
  c
= doC(.., .., a, .., b, ..);
} catch {
 
// The catch is in scope of a,b,c.
}

Which I feel I can't think of any way in Clojure to do this, which does not add accidental complexity. In that, there's nothing to the actual problem which requires say the complexity of a let inside a let, a try/catch around each steps, binding atoms, delays, promise, volatile and then setting them inside the try, etc. All these seems to me like Clojure just adds some accidental complexity to this common use case.

What do others think?

Luke Burton

unread,
Oct 2, 2017, 12:00:31 AM10/2/17
to Clojure

On Sep 30, 2017, at 3:14 PM, Didier <did...@gmail.com> wrote:

Is there another way to execute a set of complex steps which does not rely on let and can be try/catched in the manner I describe?

I can't emphasize enough the utility of the interceptor chain pattern, as employed heavily in pedestal.

I use this *everywhere* now. I have code that interleaves interceptors into a chain to augment them with timing information, audit logs, and more. I have written code that maps a subset of an interceptor chain over a cluster of machines, then reduces the results back down locally. Most importantly, they are fantastically useful when it comes time to debug a complex business process … you can swap interceptors in and out easily, isolate stateful interceptors to enable better mocking during tests, and more.

Here's a basic application of interceptors. It's really not much code considering what you get. This doesn't even scratch the surface of what they can do.

(ns interceptor.test
  (:require
    [io.pedestal.interceptor.chain :as chain]))

(def step1
  {:name  :step1
   :enter (fn [ctx]
            (let [x "sheep"]
              (assoc ctx :result x)))})

(def step2
  {:name  :step2
   :error (fn [ctx ex-info]
            (println "so much for step2:" (:exception-type (ex-data ex-info))))
   :enter (fn [{:keys [result] :as ctx}]
            
            (println "time to transform" result "into a number")
            (update ctx :result #(Long/parseLong %)))})

(defn run []
  (chain/execute {} [step1 step2]))


Didier

unread,
Oct 2, 2017, 12:21:59 AM10/2/17
to Clojure
I can't emphasize enough the utility of the interceptor chain pattern, as employed heavily in pedestal.

Interesting... Its almost like a workflow framework, but for simpler in code workflows. I'm reluctant to have a dependency on pedestal just for this though.

Daniel Compton

unread,
Oct 2, 2017, 4:06:47 AM10/2/17
to Clojure
Hi Didier

The interceptor pattern is pretty tiny, certainly small enough to copy from project to project if you wanted. You can see re-frame's implementation here: https://github.com/Day8/re-frame/blob/master/src/re_frame/interceptor.cljc which is only around 100 SLOC. That doesn't handle exceptions like Pedestal, but shows the core of the idea. 

--
You received this message because you are subscribed to the Google
Groups "Clojure" group.
To post to this group, send email to clo...@googlegroups.com
Note that posts from new members are moderated - please be patient with your first post.
To unsubscribe from this group, send email to
clojure+u...@googlegroups.com
For more options, visit this group at
http://groups.google.com/group/clojure?hl=en
---
You received this message because you are subscribed to the Google Groups "Clojure" group.
To unsubscribe from this group and stop receiving emails from it, send an email to clojure+u...@googlegroups.com.
For more options, visit https://groups.google.com/d/optout.

Duncan McGreggor

unread,
Oct 2, 2017, 8:58:14 AM10/2/17
to clo...@googlegroups.com
Didier, I've done something similar a few times just using core.async -- no extra deps required ;-)

d


--
You received this message because you are subscribed to the Google
Groups "Clojure" group.
To post to this group, send email to clo...@googlegroups.com
Note that posts from new members are moderated - please be patient with your first post.
To unsubscribe from this group, send email to

For more options, visit this group at
http://groups.google.com/group/clojure?hl=en
---
You received this message because you are subscribed to the Google Groups "Clojure" group.
To unsubscribe from this group and stop receiving emails from it, send an email to clojure+unsubscribe@googlegroups.com.

Luke Burton

unread,
Oct 2, 2017, 12:14:42 PM10/2/17
to Clojure


On Oct 1, 2017, at 9:21 PM, Didier <did...@gmail.com> wrote:

I can't emphasize enough the utility of the interceptor chain pattern, as employed heavily in pedestal.

Interesting... Its almost like a workflow framework, but for simpler in code workflows. I'm reluctant to have a dependency on pedestal just for this though.

The dependencies required are pretty minimal, you just pull in the interceptor stuff by itself. Those guys did a good job of breaking pedestal into reusable pieces.

 [io.pedestal/pedestal.interceptor "0.5.2"]                                    
   [io.pedestal/pedestal.log "0.5.2"]  
     [io.dropwizard.metrics/metrics-core "3.1.2"]                              
   [org.clojure/core.match "0.3.0-alpha4" :exclusions [[org.clojure/clojurescript] [org.clojure/tools.analyzer.jvm]]]   

Plus core.async.

I've rolled my own mini-interceptor too. If you don't need the "leave" interceptor concept and are happy with one-way execution, we're just talking about pushing functions onto a queue. Each function takes a context map that includes the queue itself. Then, implement an executor that dequeues each function and executes it. Capture any errors at this point and short-circuit execution.

Having gone down that route I just ended up using pedestal.interceptor, after reading the source code and realizing it's so simple. I like using other people's heavily debugged code :) 


hiskennyness

unread,
Oct 2, 2017, 4:37:25 PM10/2/17
to Clojure


On Saturday, September 30, 2017 at 6:14:33 PM UTC-4, Didier wrote:
I'm curious how others handle this use case, which I feel should be pretty common.

Given you have a series of business process steps, where the flow is too complex for the arrow macros, and you also like to name the step results descriptively, so you use let:

You are almost saying what I am thinking: this beast of a calculation has to be broken up (into separate functions)! :)
 

(let [a (do-a ...)
      b (do-b . a . .)
      c (do-c . a . b)]

Even in an impure language such as Common Lisp we frown on such LET forms. I believe it was Lisp legend Paul Graham who suggested we imagine a tax on LET. 

Sometimes while sorting out something complex I do solve it in a series of LET bindings, but once I understand what I am about I am able to add clarity by shuffling the code into a nice functional form (and yes, sometimes break things out into standalone functions if only to make the source more approachable).

I then think the exception handling will sort itself on its own.

-kt

Didier

unread,
Oct 2, 2017, 5:04:52 PM10/2/17
to Clojure
> Even in an impure language such as Common Lisp we frown on such LET forms

True, but as far as I know, in Common Lisp, the condition handler is always in scope of where the error happened, so I wouldn't face this problem.

I also struggle to split this up into functions without making it even more complicated. I'm intrigued by the interceptor pattern mentioned, I'll have to try it out, I worry its overkill, but not sure.

My opposition to turning this into a chain, is that it complects the order of things, with the work of each step. My steps are pure functions, they take inputs and return outputs. They don't know anything about the full workflow, just their transformation. My orchestrating function is the one which knows how to compose the smaller pure steps, so that it can fulfill the business process.

The interceptor chain seems to still complect this a little. At least it has the steps know about each other since it relies on a common context, and expects previous steps to properly assoc the data needed by later steps.

bertschi

unread,
Oct 6, 2017, 6:18:42 AM10/6/17
to Clojure


On Monday, October 2, 2017 at 11:04:52 PM UTC+2, Didier wrote:
> Even in an impure language such as Common Lisp we frown on such LET forms

True, but as far as I know, in Common Lisp, the condition handler is always in scope of where the error happened, so I wouldn't face this problem.

I also struggle to split this up into functions without making it even more complicated. I'm intrigued by the interceptor pattern mentioned, I'll have to try it out, I worry its overkill, but not sure.

My opposition to turning this into a chain, is that it complects the order of things, with the work of each step. My steps are pure functions, they take inputs and return outputs. They don't know anything about the full workflow, just their transformation. My orchestrating function is the one which knows how to compose the smaller pure steps, so that it can fulfill the business process.

No, your functions are not pure. Besides returning a value they can throw an exception, i.e. they have a side-effect!
As we know from Haskell, this forces them to be evaluated in a sequential order ... just ask yourself what value the variable c should be bound to if an exception is thrown by doB.

Both suggested solutions seem to be reasonable. While try-let ignores all bindings when an error occurs (check with macroexpand), the interceptor chain probably can see all bindings established before the error occured (I have not tested this though) and thus correctly imposes a sequential order.

Best,

     Nils

Fabrizio Ferrai

unread,
Oct 7, 2017, 7:18:08 AM10/7/17
to clo...@googlegroups.com
Now that bertschi mentioned Haskell and side-effects, I noticed that the problem we have here totally looks like a monad:
we have several steps of side-effecting computation, and each of them can fail, and when this happens you want to handle the failure while keeping the previous bindings.

Now, when it comes to handling Java exceptions in a monadic way, we can look at the Try monad in Scala, that is exactly what we are searching for.
We have several libraries that implement monadic sugar in Clojure, and `cats` [1] implements the try monad [2].

Example usage with `mlet` (monadic let, aka `do` in Haskell):

=> (require '[cats.core :as m])
nil
=> (require '[cats.monad.exception :as exc])
nil
=> (m/mlet [a (exc/try-on 1)
            b (exc/try-on a)] ;; a is visible in b scope of course, as in standard let
     (m/return (str "a: " a ", b: " b)))
#<Success "a: 1, b: 1">
boot.user=> (m/mlet [a (exc/try-on 1)
                     b (exc/try-on (+ 1 nil))] ;; when you get an exception it blows up
              (m/return (str "a: " a ", b: " b)))
#<Failure #error {
 :cause nil
 :via
 [{:type java.lang.NullPointerException
   :message nil
   :at [clojure.lang.Numbers ops "Numbers.java" 1013]}]
 :trace
 ...
=> (m/mlet [a (exc/try-on 1)
            b (exc/try-or-else (+ 1 nil) a)] ;; but you have the a binding also in case of exception
     (m/return (str "a: " a ", b: " b)))
#<Success "a: 1, b: 1">



Cheers,
Fabrizio

--

Nathan Fisher

unread,
Oct 7, 2017, 9:32:39 AM10/7/17
to clo...@googlegroups.com
You could use exceptions, is that a hard requirement or are you working to transition your mental model for Java code to Clojure?

If you can catch the exceptions or not throw them to begin with there’s some other options;

Another way you could do it is using core.async and channels as some others have mentioned.

Yet another way would be using cats.

A similar solution to what the problem you described is outlined partway through this article (there’s a lot there):


A friend of mine used to say “get it writ, then get it right”. As long as you encapsulate the desired behaviour in the function you can try all of the different suggestions and take a decision of what feels more readable to you and others on your team.

--
You received this message because you are subscribed to the Google
Groups "Clojure" group.
To post to this group, send email to clo...@googlegroups.com
Note that posts from new members are moderated - please be patient with your first post.
To unsubscribe from this group, send email to

For more options, visit this group at
http://groups.google.com/group/clojure?hl=en
---
You received this message because you are subscribed to the Google Groups "Clojure" group.
To unsubscribe from this group and stop receiving emails from it, send an email to clojure+u...@googlegroups.com.

For more options, visit https://groups.google.com/d/optout.
--
- sent from my mobile

Gary Verhaegen

unread,
Oct 8, 2017, 9:49:45 AM10/8/17
to clo...@googlegroups.com
As others have noted, this is pretty much what monads are made for. I've found Brian Marick's "Functional Programming for the Object-Oriented Programmer"'s chapter on monads really good at teaching how to recognize situations where monads would be a good fit.

For this specific use-case, you can probably, as an intermediate solution, define a fairly simple macro that would get you almost all you want, something like:
(defmacro mlet
  [bindings & body]
  (if (seq bindings)
    (let [[b expr & r] bindings]
      `(try (let [~b ~expr]
              (mlet ~r ~@body))
            (catch Exception e#
              (let [~b [:mlet/error e#]]
                (let ~(->> r
                           (partition 2)
                           (mapcat (fn [[b _]] [b :mlet/unbound]))
                           vec)
                  ~@body)))))
    `(do ~@body)))

(mlet [a (+ 1 2)
       b (- a 3)
       c (/ 10 b)
       d (inc c)]
  [a b c d])
;; => [3 0 [:mlet/error #error {#_elided}] :mlet/unbound]
Depending on how much complexity you're willing to bear within that macro, you could do smarter things like detecting which variables can still be computed, etc.

Maybe also take a look at prismatic graph? https://github.com/plumatic/plumbing
Reply all
Reply to author
Forward
0 new messages