Fork me on GitHub
#clojure
<
2023-07-07
>
ignorabilis14:07:27

Hello fellow clojurians, I have a question that might stir a bit of controversy: Throwing exceptions vs returning nil/error maps Personally I hate exceptions as they feel very much like GOTO - fn a calls fn b, which calls fn c - but then boom, fn c throws an exception, you lose control of the flow (pun intended) - and if that exception is not caught you end up in a very different part of your application - one that might lack context etc. and now you're in the whole business of "where do I catch that exception properly" or "what the hell just happened"... Admittedly though many times exceptions in simpler contexts just work. In other words exceptions to me are easy but not simple - when writing the code they are easy because if you encounter a "difficult" situation you can just make a hand-wavy motion and throw an exception - done. But they are not simple, because they need to be caught somewhere else in your application, sometimes blow up in unexpected ways and oftentimes debugging those can be quite hard. And if you process data and you want to send the results of that processing anywhere - you can't send exceptions; there's a lot of overhead when dealing with them, catching them, debugging them, etc. Otoh returning nil/error map is simple but not easy - if you return nil you need to make sure that your functions have not overloaded nil with other meanings. For example a function that checks something might return true if it found it and nil if it didn't. If that function is making say an http call it might error out, but now returning nil might mean "I didn't find it" or "I couldn't even reach the server - but if you tried again I might find it"; so you change the function to return true, false and nil - which are the proper states - but a lot of devs would write (when (fn-check) ...), and in that context false equals nil unfortunately - and now you're forced to return an error map to disambiguate between nil and false. Returning error maps is fine, but a lot of times it may force your code to become littered with (if error statements - simple but very repetitive, time consuming and definitely not easy. And using when or if still might suffer from the same issues... Then you might join a team where people are already inclined to use exceptions the Java way™ - and if the codebase is already littered with those pesky little exceptions good luck making the code more functional... So the first question is what are your opinions - exceptions or nil/error map - which is more idiomatic? If the latter - should your Clojure code throw an exception, ever - and under what circumstances? (Note - handling Java exceptions is unavoidable, either because of interop or just because your code might try to do something stupid - and being on top of JVM means exceptions will be thrown, unfortunately). I obviously already have an opinion - but if you favor exceptions can you think of a good example of why to use them (apart from "it's easier to just throw")? Also any good practices for returning nil/error maps? Libraries? Use https://github.com/cognitect-labs/anomalies?

👀 4
6
👍 4
p-himik14:07:09

> which is more idiomatic? Neither. There are as many opinions on specifics as there are people here, it seems. Personally, I don't have a principle, nor do I have a decision tree to choose which approach I want to go with. Rather, I go by intuition. Way too many variables for me to come up with an unambiguous and satisfactory decision tree.

cjohansen14:07:45

Generally I don’t think generic utilities and “library code” should throw exceptions, but instead either do nil-punning where suitable, or return more rich descriptions of errors/problems. However, I do find careful use of exceptions in high-level business processes acceptable - especially when using ex-info to carry data with the exception. I tried pretty much all the alternatives here a few years ago, and was originally very anti-exceptions and pro custom data for communicating errors. This led me to try libraries like https://github.com/adambard/failjure and others like it. Eventually though, it occurred to me that I had really just replaced the try/catch built-ins with an extra third party dependency and custom-made macros that did basically the very same thing. So I reverted out of the experiment and found piece with careful use of exceptions in high-level code where I do not want the code to continue running past an error.

👍 2
ignorabilis15:07:06

yeah, just saw the thread by Paavo, going through it 🙂

cjohansen15:07:17

As a side-note I try to steer away from any pattern that makes my application dependent on third-party libraries for basic control-flow and core concepts. I don’t want a single library to permeate my entire code-base. It raises the bar of entry for other developers, makes you overly reliant on one dependency, and steers you away from simple clojure.core-based solutions.

17
👍 2
ignorabilis15:07:00

yep, agreed on that one; not just for exceptions but for almost anything really - the closer to clojure core your code is the easier to understand is by everyone

💯 5
Evan Bernard15:07:37

I like exceptions, and I do indeed come from the java world. that being said, I’ve seen them used and abused in many, many cases I wouldn’t personally call ‘exceptional’ 🙂

ignorabilis15:07:00

@U053URTDGDD - what would you consider exceptional? http 404? 403? 429? unable to establish db connection at application startup? unable to establish db connection for a specific user request? malformed sql query? division by zero?

Mario G15:07:28

I tend to: • default to nil punning/err msg(or map) most times • bite the bullet and accept try/catch/throw if the codebase I'm changing is to difficult to adapt to a non-throw approach; • complain a lot and being the anti-throw advocate if the try/throw/catch web is just too messy 😄 thanks to nil punning and other sensible defaults in Clojure land (eg side effects mostly isolated at the edges of a system - such as api calls, db connections - almost only pure functions everywhere else) I have yet to see a Clojure codebase which is seriously compromised by try/catch pollution, that is more the case in Java/C#/JS land. I haven't tried a monadic approach yet (failjure, o even better, to try it in languages which embrace natively that approach) in a work environment so I can't express a preference here, but I'm definitely curious about it.

Evan Bernard15:07:20

• http responses: ◦ am i handling them? if so, probably not. they’re so non-exceptional, it neatly fits into a sort of error map, maybe not unlike what you’d proposed - {:status 400} 🙂 OTOH, if my app can do nothing without a 2XX, then that may indeed be exceptional for my app ◦ am i sending them? if so, certainly not • unable to establish a db connection ◦ do i expect this situation and have a backfill/cache to use? if so, certainly not ◦ if not, and i’m dead in the water without that, probably exceptional ◦ for a specific user request: ▪︎ i would likely try to shy away from exceptions, especially if i’m trying to maintain a high throughput. those stack traces are expensive! in any case, i feel like most db libraries i’ve used treat this as exceptional anyway. do i re-throw? if there was a way i could afford not to so as to quickly retry the request, or signal to the consuming API that they should retry soon, i’d rather do that EDIT: i’ll leave it there for now so as not to clog the thread, but that’s some of my back-of-napkin line of thinking about this

👍 2
Mario G15:07:48

> I’ve seen them used and abused in many, many cases I wouldn’t personally call ‘exceptional’ exceptions for control flow, I suppose 🙂

stand15:07:50

A good case for exceptions in clojure came up in my work yesterday. I have a function foo* that creates an object based on the input parameters. I also have a function foo which is just (memoize foo*). If (foo* :a) fails for some reason, I do not want it to return nil because I don't want (foo :a) to cache a nil. Rather, I want (foo* :a) to throw an exception so that nothing is cached for a memoized call.

dgb2316:07:08

My rule of thumb is that exceptions are broken assumptions. I don't like to throw them except I want the program (thread/process etc.) to actually crash. When throwing an exception I'm saying: "I can't do anything further here, but here's some information that might help to fix the problem." A caller should be able to choose whether, when and where they handle it. In the general case, I rather return a value that describes w/e the state is. In that case the 'error value' is part of normal control flow. I'm saying: "Here is some information about the program state."

dgb2316:07:52

I think there are some languages where you want to throw exceptions all the time, because your IDE jumps in there and lets you fix the state on the fly. There exceptions are part of interactive programming. But I don't think Clojure offers this.

kwladyka19:07:57

I didn’t read all this text, but I would say: 1) remember catch doesn’t have to be (catch Exception ... to cach everything. You can use specific class to handle exception in right way. You really should. This already solve most of issues. 2) But on some level you catch all and you expect unexpected exceptions. This is fine. 3) Use cause instead of re-throw exception as it is. 4) add debug data with ex-info 5) in API which has clear response error to help developers understand what is wrong it is fine to throw exception and later on, on the top level check (= :validation (:type (ex-data error))) to identify exception as expected and convert error to human readable form informing about for example connection timeout to third party system or some not valid data deeper in the code. Why? Because otherwise each function in the flow has to know how to differ expected data vs error data. Exception cut the flow immediately and from deep deep fn you are immediately in the code which decide what to return in response in such case.

👍 2
pppaul21:07:00

in elixir you don't have a choice, so error objects are a successful way of doing this, but in java/clojure you are going to end up in a mess of both types, at least at some level.

Ludger Solbach08:07:59

In clojure code, I nearly never invent/throw exceptions (with the exception of a math lib I wrote (pun intended) 😉). Even in Java I rather have the caller decide, if something is exceptional or not. For example in validation functions I return a collection of validation errors and the caller can decide to handle them in the normal flow of control or to throw an exception. On retrieving external state by some criteria (e.g.), I provide a 'find' and a 'get' method. In 'find' it's ok, if nothing is found and null/nil is a valid return value, in 'get' a result is expected and an exception is thrown, when the state is not there.

valerauko14:07:43

i avoid exceptions whenever possible (unless a library i use throws them and i have no control over that) especially because the significant performance penalty of building an exception. i was shocked how slow it is.

ignorabilis06:07:50

@U053URTDGDD - you say you like exceptions, but from your description it seems you use them quite sparingly... I don't like them, but at least superficially I use them in a similar way; for example I try to not use exceptions as a general rule, but as you say if I'm dead in the water and the alternative is to change every function along the chain to check for nil/error (during system startup for example) - I'd just throw an exception as well; http/db/etc requests and user requests are not exceptions, we should be expecting issues with connectivity/discoverability/etc; I guess the only difference is if my app can do nothing without a 2XX, then that may indeed be exceptional for my app - I'd try to return something more meaningful rather than throw

ignorabilis06:07:10

@U017QU43430 - I feel for you, I've ended up being the anti-throw advocate a few times 😅

😆 2
ignorabilis06:07:56

@U0608BDQE - that's interesting; so you avoid memoization because if foo* gets retried it might succeed?

kwladyka12:07:24

just to extend my previous answer: Personally I don’t have any issues with exceptions if these is done in the way how I described. But I have issues with exceptions because of team work. People do code in different quality and this also apply to exceptions mess. Usually developers don’t spend enough time to learn how to do this things in high quality for exceptions and logs. The topic itself is deeper, than it looks on first glance. Not investing time to learn how to log, throw and catch exceptions is the issue. Not exceptions itself. After all exceptions are not only to not crash app, but to alert developers and provide data to understand what happen. This is the most important thing.

kwladyka12:07:50

So there are 2 types of exceptions: 1) expected exceptions - if input data is not valid or third party system fail because of network connection etc., then throw exception and return 0 or current time as default value 2) unexpected exceptions - alert developers and provide data to fix the issue For 1) we should also measure exceptions, because too many expected exceptions is unexpected and means not expected bug :)

ignorabilis12:07:08

@U0WL6FA77 - I'd argue that if you're expecting an exception it's not really an exception - it becomes an ordinary issue, not something "exceptional"; and most exceptions, even in OO land, are thrown in unexceptional situations

💯 2
kwladyka12:07:10

For example I expect exceptions for third party bookeeping and warehouse system, because often it has timeout. I want to log it, I want to measure it, but I expect this behaviour, so I don’t want to be alerted. This is not something possible to fix. It is something to live with.

kwladyka12:07:23

but if it is happening too often, then it needs attention and became unexpected.

kwladyka12:07:14

For example they changed API to add extra data in response which increase timeout from few % to 90% which made system not functional.

kwladyka12:07:11

the same technical exception, but in different numbers made it unexpected

kwladyka12:07:17

> I’d argue that if you’re expecting an exception it’s not really an exception It is still exception from technical point of view. But as a concept it is expected exception and from that moment you can code your system to handle that in expected way.

kwladyka12:07:50

> code your system to handle that in expected way. this is the point for type 1)

Evan Bernard12:07:07

replying to https://clojurians.slack.com/archives/C03S1KBA2/p1688971370565369?thread_ts=1688741187.820029&amp;cid=C03S1KBA2 above - I like fire alarms, airbags, and parachutes, but I try to use them sparingly, as well 😉 (though I surely use exceptions a bit often than those…)

😅 2
✔️ 2
kwladyka13:07:35

to extend what I mean for 1). In the moment of throwing exception you can’t determine if this is expected or unexpected. The conclusion is delayed until your aggregate data.

Mario G13:07:16

> I like fire alarms, airbags, and parachutes, but I try to use them sparingly very well put 👌

Caio Cascaes15:07:36

I've used to run lein cloverage - I know, hot discussion - I want just to find important unit tests or integration test that are good to cover. Is there a version for deps.edn'ed projects?

lukasz15:07:05

Kaocha has a cloverage extension: https://github.com/lambdaisland/kaocha-cloverage

💯 4
t-hanks 2
noonian15:07:47

I believe you can also use coverage directly with deps cli https://github.com/cloverage/cloverage#clojure-cli-tool (the dep should be coverage/coverage to avoid errors about unqualified libraries). I'd recommend creating an alias in deps.edn with the dependency and run config so you can run it with something like clj -M:coverage or similar

Caio Cascaes15:07:38

With kaocha: Adding in deps.edn

:kaocha  {:extra-deps {lambdaisland/kaocha {:mvn/version "1.1.89"}}
                     :main-opts  ["-m" "kaocha.runner"]}}}
And adding a file in root - tests.edn
#kaocha/v1
{:tests [{:id :unit
          :test-paths ["test"]
          :plugins [:kaocha.plugin/cloverage]}]} 
I'm getting:
$ clojure -A:kaocha
Error building classpath. Could not find artifact lambdaisland:kaocha:jar:1.0.75 in central ()
Not sure if I'm doing something wrong.... Am I missing something or misconfiguring?

Caio Cascaes16:07:54

I managed to work with Cloverage, thank you @U052PH695! I also want to achieve the same with Kaocha. I'll try to solve the issues by myself, but @U0JEFEZH6, if you have any tips on how to solve them, I would be grateful!

vemv16:07:03

You can also use https://github.com/practicalli/clojure-cli-config as reference for all things tools.deps (yes, it has specific Cloverage coverage)

👀 2
Noah Bogart17:07:42

I have a sequence of pairs (`([1 2] [3 4] [5 6])`) and I want to get a set of the first items and a set of the second items. Is p1 (set (map first coll)) p2 (set (map second coll)) really the best way?

Noah Bogart17:07:04

lol i know it's a simple two lines and i shouldn't bother, but going over the list twice makes my eye twitch, but using reduce or loop also makes my eye twitch given how much more code i'm adding

frozenlock17:07:25

(apply map #(into #{} %&) '([1 2] [3 4] [5 6]))

👍 8
2
p-himik17:07:50

Not comparing O(2*N) to 2*O(N) makes programming much more fun. :) I'd definitely go with the most explicit solution, regardless of the fact that it creates two lazy sequences.

👍 2
vemv20:07:51

(reduce (fn [sets [a b]]
          (-> sets
              (update 0 conj a)
              (update 1 conj b)))
        [#{} #{}]
        '([1 2] [3 4] [5 6]))
Personally, not seeing this clarity would make my eye twitch

👍 2
Ed12:07:30

just to throw in another option, if you're prepared to use cgrand's xforms lib, you could reach for transjuxt (which is a tripple word score in clojure-scrabble)

(sequence (comp (let [xf #(comp (map %) (x/into #{}))]
                    (x/transjuxt [(xf first) (xf second)]))
                  (mapcat identity))
            [[1 2] [3 4] [5 6]])

🤯 2
👀 2
Jason Bullers20:07:21

How about apply map hash-set '([1 2] [3 4] [5 6]) so you don't need the anonymous function?

😮 2
frozenlock22:07:29

Indeed! Good catch.

👍 2
reefersleep09:07:49

Very neat, @U04RG9F8UJZ. Thanks everyone, I enjoyed this thread 🙂

Jason Bullers15:07:18

@U0AQ3HP9U thanks. This is something I just learned recently through Advent of Code. In general, taking advantage of the fact that map accepts multiple collections and passing the nth element through to a varargs mapping function (i.e. apply map some-varargs-fn coll-of-colls) effectively let's you transpose the coll-of-colls. It was really neat to see that pattern. I did it with str as the mapping fn, but it fit so neatly here too to transpose a collection of entries to a collection of keyset valset.

reefersleep20:07:02

@U04RG9F8UJZ it really is neat functionality of map that I also forget to use.

p-himik21:07:34

IMO the fact that many people forget that (apply map ...) is effectively transposing the data might indicate that it's not something that should be used. At least, when corroborated with actual measurements of frequencies in OSS code. Or it can signal the need for better documentation of that pattern. But I still can't fathom how it could even potentially be more graspable at a glance than the code in the OP. (Apart from that, the code works differently if there are items with less than 2 items - unclear whether it's important for the OP but definitely important in general.)

👍 2
Noah Bogart21:07:47

i tend to not like using the var-args version of map cuz it works so much differently than map functions do normally (in other languages). are there any other functions that work the same within clojure?

Jason Bullers21:07:31

@U2FRKM4TW personally, I find it fairly intuitive :man-shrugging::skin-tone-2: I certainly won't argue more so than the original code snippet, but certainly more so (to me) than the other proposals in this thread. There's an example of transposing a collection of vectors as part of the first code sample on Clojure docs, so it's out there in docs at least. Really good point about the number of items: there's that extra constraint that all the inputs are indeed pairs (in this case). Irregularly sized collections would give incorrect results as data would be dropped

Jason Bullers21:07:45

To be clear, I'm a Clojure newbie, so I don't really know how this compares to idioms in the wild. It seemed a likely one to me after reading the docs and reading about the cool pattern of into and mapping with juxt for transforming one map to another. I got the feeling that economy of code was useful in clearly communicating an idiom (not terse for the sake of terse, but economical such that there's less pieces to learn and understand to grok how it works) as it ends up easier to identify at a glance

Jason Bullers22:07:38

On the subject of idioms, is there ever a situation where you'd apply map and are not mapping across a bunch of collections (e.g. transposing)? What I'm wondering is: is it safe to infer a particular meaning from seeing apply map (regardless how common it is to see), or is there more than one use case (and perhaps a more common one than this)?

p-himik22:07:50

Can't think of any since all arguments but the first are supposed to be collections. Except for toggling between the 1- (transducer) and 2-arities when given an empty collection or a size-1 collection. But that'd be bizarre.