7 ms·
I loved the either-monad solution. The problem is something I've experienced many times in my clojure adventures, usually resulting in either dirtying the funct
by jtmarmon 10y ago
I loved the either-monad solution. The problem is something I've experienced many times in my clojure adventures, usually resulting in either dirtying the function in question with its own, case specific logging or creating the if-let monstrosity you demonstrated.
As a side note: there's something very comforting when you immediately empathize so strongly with a programming problem someone is writing about
- lackbeard 10y agoThat part really resonated with me as well. Well the whole article did... it really matches my professional experience with Clojure (except I've never looked at transducers.) I still don't feel great about error handling in Clojure in general. I can't find something that feels both practical and idiomatic. I want to use monads but it always feels like huge impedance mismatch with the rest of the language (most of the time due to nil being semantically overloaded.)
- sdegutis 10y agoFor 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)))
- kazinator 10y agoGiven 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.
- sdegutis 10y agoYour 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.
- kazinator 10y agoYes; 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.
- egamble 10y agoI wrote something like that several years ago: https://github.com/egamble/let-else https://github.com/egamble/let-else
- sdegutis 10y agoReally cool! May have to borrow this some day.
- SomeHacker44 10y agoI 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).
- jcadam 10y agoI've been using the cats library myself: https://funcool.github.io/cats/latest/ https://funcool.github.io/cats/latest/ and find it really useful. I mostly use the Maybe and Either monads (and occasionally the exception monad if I'm calling some function in a third-party library that might throw an exception).
- tel 10y agoHaving used the cats library in anger... I think for Clojure it'd probably be better to make a couple specific monads to handle, e.g., either-ing. Talking about monads abstractly really requires type support, I've come to believe. It can be done otherwise, but it gets really complicated.
- losvedir 10y agoOof, I don't know. I love Lisps, generally, having gotten my start with Scheme back in the day. But that monadic thing just rubs me the wrong way. I don't program Clojure, though, so maybe it's just lack of experience. If they had gone with Rust's Ok/Error rather than Haskell's Left/Right, maybe it would be better to me. (Yes, yes, I know, the monad is more general than that, but if it's used 99% of the time for handling errors, maybe optimize for that use case.) I program in Elixir now, which I'm totally loving. It's also a "no early return" and "totally immutable" language, so I wonder if any "cross pollination" between the languages is possible. This is how I would code the example given in Elixir: with {:ok, x} <- foo(), y when not is_nil(y) <- bar(x), {:ok, z} <- goo(x, y) do qux(x, y, z) IO.puts "it worked" true else {:error, :not_fooable} -> IO.puts("foo failed"); false {:error, :no_bar} -> IO.puts("bar failed"); false {:error, :no_goo} -> IO.puts("goo failed"); false nil -> IO.puts("something (bar?) returned nil"); false _ -> IO.puts("some other error"); false end Elixir's cool "with" / "else" macro is used for chains of calls where the intermediate steps can fail. In fact, it was introduced not too long ago, just to deal with nesting code issues like those raised here! It relies on elixir/erlang's pattern matching to specify the happy path, extracting out the success responses (e.g. {:ok, x} will match a response of {:ok, 5}, and x will hold the value 5). If any of the happy matches fail, it falls through to the `else` and you can catch any particular failures you want there. It's a really great system. Someone should try to add it to Clojure! Not sure how well the "pattern matching" aspect of it can work, though.
- abecedarius 10y agoIt looks like this in my personal Lisp dialect, with no new macros: (match (foo) (#no (log "foo failed") #no) (x (match (bar x) (#no (log "bar failed") #no) (y (match (goo x y) (#no (log "goo failed") #no) (z (qux x y z) (log "it worked") #yes)))))) I imagine any language with a match macro, like Racket, could look similar. Using monads or exceptions ought to look nicer, but I'd kind of rather not pull out the big guns.
- deleted 10y ago[deleted]