As a side note: there's something very comforting when you immediately empathize so strongly with a programming problem someone is writing about
As a side note: there's something very comforting when you immediately empathize so strongly with a programming problem someone is writing about
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.
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.)
(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. (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))) ;; 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.Substitute your favorite dialect's error thrower.
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.
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).