Hacker News new | ask | show | jobs
by sdegutis 3369 days ago
For the nested if-let mess, I'd probably do something like this:

    (let-every [x (foo)     err "foo failed"
                y (bar x)   err (format "bar %s failed" x)
                z (goo x y) err (format "goo %s %s failed" x y)]
      (qux x y z)
      (handle-error err))
Where `let-every` is a macro that works like let, but stops short on the first nil/false variable, runs only the next symbol binding expression, and then runs the else-clause.

There'd be nothing special about the "err" symbol on each line. It's just the next symbol binding, but on the same line as a convenience, and this means it can reference any previously valid symbol bindings.

Here's a quick & dirty implementation of that macro. I don't have a Clojure interpreter installed, so I don't know if it works.

    (defmacro let-every [bindings if-body else-body]
      (let [pairs      (partition 2 bindings)
            quad-pairs (partition 2 pairs)]
        (loop [quad-pairs quad-pairs]
          (if quad-pairs
            (let [[quad-pair]         quad-pairs
                  [try-pair err-pair] quad-pair
                  [try-sym try-expr]  try-pair
                  [err-sym err-expr]  err-pair]
              `(if-let [~try-sym ~try-expr]
                 (recur (next quad-pairs))
                 `(let [~err-sym ~err-expr]
                    ~else-body)))
            if-body))))
Given the above example, it should expand to this:

    (if-let [x (foo)]
      (if-let [y (bar x)]
        (if-let [z (goo x y)]
          (qux x y z)
          (let [err (format "goo %s %s failed" x y)]
            (handle-error err)))
        (let [err (format "bar %s failed" x)]
          (handle-error err)))
      (let [err "foo failed"]
        (handle-error err)))
4 comments

Given the existence of exception handling, I would rather apply the pattern:

   ;; Plain old ANSI Lisp

   (let* ((x (or (foo) (error "..."))))
          (y (or (bar x) (error "bar ..."))
          ...)
     body)
I mean, if, in the end, we are going to "error out", rather than just forward-propagate `nil`.

If we are going to just propagate `nil`, then if we have an `iflet` that tests the last variable, we can do:

  (iflet ((x (expr))
          (y (if x (bar x))
          (z (if y (foo x y))) ;; z is tested by iflet
    ...)
it's just a bit verbose. That can be condensed with a simpler macro that doesn't have the err stuff.
Your solution doesn't short-circuit when any of the calls fail. It relies on (error) throwing some kind of exception to interrupt control flow and prevent the following lines from executing.
Yes; I'm using ANSI CL syntax/semantics. error denotes cl:error which can in fact be relied upon to throw. That's what I mean by "given the existence of exception handling ...".

Substitute your favorite dialect's error thrower.

I wrote something like that several years ago: https://github.com/egamble/let-else
I really like `:delay`. It will allow efficiencies in my already Haskell-esque "define all possible values used in a single big let" style I program in. My code all handles nil safely and returns nil when appropriate but it would be even better to avoid evaluating things unless used in the body of the else (or a later binding).
Really cool! May have to borrow this some day.
OP already conceded if these funcs return nil the solution is easy; the case considered was when the funcs return heterogeneous data
Or when you want to do logging. That's the part I was working at.
>Given the above example, it should expand to this:

I don't think that your expansion is correct at all. Maybe you have a misplaced quote somewhere, but this one is not making sense.