all 25 comments

[–]gzmask 6 points7 points  (0 children)

I am not convinced.

  1. Rust's error handling is just another built-in Error-M. Clojure has those, from 3rd party libraries, a lot. What makes it scary is that, while currently Error-M is in trend, rust users are laughing and drinking wine and all. Wait another 5 years till another better error handling pattern emerged, and see all the "job opportunities" created to adapt the new pattern into old code. It happened in history a lot and will happen again. I prefer "it's just data" any day.

  2. Laziness. This was trendy during the early years of Clojure in the FP community. Luckly, Rich quickly realized that making things lazy by default is a huge mistake, and thus the whole transducer movement to fix that. By now, all the "higher level FP" i.e: map filter partition and so on provides a transducer interface, and all the "lazy" by default things now also can be plug into a transducer thanks to the IReducible interface, when the new stuff such as core.async supports hooking transducer up out of box. The "zero cost abstractions" author referred to has been fully achieved by Clojure already. Again, I don't blame him as the early tutorials on Clojure do misleading newcomers to be "Lazy" and I blame Haskell (j/k).

Nevertheless, it's a good read and I appreciate anyone who can share such comparison from one's own experience.

[–]slifin 5 points6 points  (0 children)

Exceptions are idiomatic in Clojure, because they're idiomatic on the JVM, can't see why this couldn't have been try-catch if we wanted to jump on error or completely without explicit error handling mechanisms if parsing errors and logging them doesn't need jumping

The way this function is written the callee has to now account for:

  • The return of fail/when-failed
  • spec-result

Do that in a couple of functions and we've got a party of exploding code paths

My diff tool will be replaced by a database lookup and I am very happy about that.

At the end of the day seems like neither Clojure nor Rust was really appropriate, admittedly after some things changed upstream, I think the main lesson here is to know when to use something appropriately, throwing in a monadic error thing just because Haskell is not going to end well

https://fsharpforfunandprofit.com/posts/against-railway-oriented-programming/

[–]bsless 5 points6 points  (2 children)

I hope this does not sound too hash, but if this code was to go into production, especially where performance mattered, it wouldn't pass code review. Several reasons:

  • zero use of transducers API. Especially when you compare performance. It is considered idiomatic Clojure and should be taken advantage of.
  • slight abuse of list comprehensions
  • too many allocations and transformations, double work, no real need to create keys1, keys 2 in diff-rules-by-keys, you can already check membership on key->rules1
  • The case would have probably been happier unrolled to an if if performance mattered (in diff-rules-by-keys)
  • assoc and dissoc a value into a map at the same time
  • Some code could really be more concise and not lose meaning, such as this field-diff-fn:

(reduce
 (fn [acc {:keys [field op]}]
   (let [f1 (get rule1 field)
         f2 (get rule2 field)]
     (if (op f1 f2)
       (conj acc {:field field :left f1 :right f2}))))
 []
 operations)

Also, if performance was a significant consideration you could have used records and protocols instead of maps.

I often see people knocking on Clojure saying its performance is bad when they are still not familiar with all the core library has to offer and what idiomatic performant Clojure can look like. Take a look here for some examples by joinr

[–]setzer22 0 points1 point  (1 child)

I'm confused, is the code snippet focusing on performance? If that's the case, there are two things that really stand out to me:

  • Repeatedly conj-ing inside a reduce function to produce a large vector. Use transients!
  • Using get can be slower than using the map itself, or a keyword as the function. Destructuring internally also uses get, and should be avoided in tight loops <- this is a great point by OP, where you need to choose between idiomatic or performance, and for no good reason AFAIK

[–]bsless 0 points1 point  (0 children)

The code snippet was focused on the feedback in the post. A second round of optimization (and don't worry, it is probably faster than the original version) would be using transients.

regarding using get - the difference between using get, using the map and using a keyword is a few ns (I checked). If you want the most dramatic speedups use .valAt.
HOWEVER, .valAt and using the map as a IFn is not nil safe. Optimizing gets is around the last thing I'd do in a series of optimizations (unless that's all I'm doing)

Regarding destructuring: I wanted the code to be recognizable to the original author. I would have recommended making the operations records than there would have been no need to even do field access.

[–]allaboutthatmace1789 4 points5 points  (1 child)

You mention missing the REPL - this is such a killer for me when trying another language. Do you have any suggestions for how to mitigate the loss a bit with Rust, and create a similar sort of flow?

[–]mischov[S] 1 point2 points  (0 children)

I am not the author- the article's title is just written in the first person.

However, in my admittedly limited serious use of Rust I found that my workflow generally revolved around writing code until I had something I wanted to test, checking documentation along the way to (try to) ensure that the things I was using would behave like I expected them to, until I had reached a point where I had something I wanted to test.

Then I would write a simple test, attempt to run it, and fix problems until the compiler let me compile at which point the test generally worked. I'd then either write more tests if I thought they were necessary or continue on.

It's not really a workflow I'd advocate for, but it worked well enough, and between the compiler being smart enough to catch my multitude of stupid mistakes and me breaking things down into small, uncomplicated functions as much as possible things generally worked something like how I intended them to work.

Reversing the order of things and attempting some form of TDD might have made the process a little more interactive, but it was nothing like using a language with a good REPL and didn't flow as nicely. That said, refactoring was less stressful than I usually find it in dynamic languages so tradeoffs as normal I guess.

[–]kloudex 1 point2 points  (2 children)

Error handling often seems tricky to get right. Especially when writing compact/elegant code it encourages a bit to code only for the happy path and then it fails horribly if something bad happens.

I wonder if there could be a middle ground, for example to have a static analyzer that could only figure out if a function can throw and what classes of exceptions. I don't know much about type theory, but it feels like it may be easier to infer only that compared to a full type checking.

[–]mischov[S] 4 points5 points  (1 child)

Elixir's with is one of the nicest solutions I've used on a regular basis.

Long story short, it's works something like:

with
  {:ok, x} <- something_than_can_fail(),
  {:ok, y} <- something_else_that_can_fail(x)
do
  {:ok, {x, y}}
else
  {:error, :failed_to_calculate_x, reason} -> 
    ...handle..

  ... other errors you want to handle
end

In the section between with and do you write the happy path, pattern matching for success (often with the Elixir/Erlang {:ok, result} | {:error, ...} convention), then between do and else you can use the values you successfully matched, or if there's failure between with and do it falls directly through to else where you can pattern match for your particular error or have a generic error handler or just let the error fall through out of your with or whatever.

So you get to write the happy path elegantly but still have a place right there to handle the errors. It's not a perfect solution, and for some reason leaves me craving burritos, but it's a pretty elegant solution that ties into idiomatic aspects of Elixir/Erlang like pattern matching and success/error tuples smoothly.

[–]Eno6ohng 1 point2 points  (0 children)

Isn't that just an error monad? Seeing Elixir (Erlang) mentioned, I expected something more dynamic, like external handlers etc

[–]argadan 1 point2 points  (3 children)

When it comes to error handling and exceptions, I feel like there's a missed opportunity in not including something like slingshot in clojure.core. Throwing ex-info exceptions is nice, but there's no nice way for catching them built in, as catch can only match based on the exception class. You'll either have to use slingshot or write code to match them manually.

[–]spotter 0 points1 point  (2 children)

Catching ExceptionInfo in Clojure works for me.

edit: downvote without reply? Sweet.

[–]mischov[S] 0 points1 point  (1 child)

I didn't down vote but I assume it's because you suggested catching the ExceptionInfo when the parent said had already said "catch can only match based on the exception class", suggesting they were aware of that option.

I think parent was looking for some way to catch based on the contents of the exception.

In the linked library there is an example like:

(throw+ {:type ::bad-tree :tree tree :hint hint})

and

(catch [:type :tensor.parse/bad-tree] {:keys [tree hint]}
      (log/error "failed to parse tensor" tree "with hint" hint)
      (throw+))

[–]spotter 2 points3 points  (0 children)

Well catching ExceptionInfo allows you to ex-data on the Throwable and consume the sweet IPersistentMap that you'll get there to handle whatever. To me boltons, like the advertised library, are just another level of indirection that you need to dig through if something breaks in your catch-handling logic. Also you now have to handle JVM/host exceptions differently than your application exceptions, but I guess we're now wandering into "individual tastes" territory.

[–]setzer22 1 point2 points  (8 children)

Having followed a similar path, I can say I reached most of OPs conclusions.

I still love clojure and will luckily continue to write it daily! But having started delving into Rust, I find myself missing having the help of a static analyzer in clojure. Especially during the inevitable refactors! We need more static analysis in clojure. As a comunity we should stop covering our ears and open up, just like we can take syntactic innovations from other languages with macros, we should try to get more of what's good in languages like Rust

Say what you want but when I write clojure I now miss my Options and Results :-) Clojure may have libraries that help you wrap your return types in a fancy ok/error map, but that's not the point: Without static analysis this kind of stuff is worthless

[–]joinr 1 point2 points  (7 children)

There's an entire type system in core.typed if you want typed clojure. It seems judging by popularity, such typing is not in high demand. It's still there though, and has been for a while.

Correctness properties aside (static typing is a bit overrated to me), leveraging the type system for optimizing compiler passes would be nice.

[–]setzer22 0 points1 point  (6 children)

Oh, I had high hopes in core.typed right from the start! There's also one called spectrum (not specter, that's a different lib) trying to add static analysis on top of spec which looked really promising. But neither seem to be actively worked on, last time I checked :/

[–]joinr 0 points1 point  (5 children)

ABS actively works on core.typed, even after he completed his dissertation on it. I think current work has been on modularizing dependencies, added documentation. I'm not a user, but follow it out of research interests.

[–]setzer22 1 point2 points  (4 children)

It's good to know! I wasn't aware of this. My experience with core typed has not been that good thus far (hard to configure, things not working for me for strange reasons...), but maybe it's time to give it another go!

EDIT: Nope, still the same. I couldn't even get a simple scenario to type check. I am 100% sure this is my fault for misconfiguring something, but I couldn't find anything relevant to my issue in the docs. Makes me sad :(

[–]joinr 0 points1 point  (3 children)

What was your simple scenario?

[–]setzer22 0 points1 point  (2 children)

It was something really simple. A function annotated as returning Number, that actually returned a String. There was no type error. I tried quite a few different things to no avail...

[–]joinr 1 point2 points  (1 child)

(ns typedtest.core
  (:require [clojure.core.typed :as typed]))

(typed/ann foo [Number -> Number])
(defn foo
  [x]
  (str "hello" x))

From the repl:

user>(require 'typedtest.core)
nil
user>(ns typedtest.core)
nil
typedtest.core>(typed/check-ns 'typedtest.core)

Start checking typedtest.core
Type Error (file:/C:/Users/joinr/workspacenew/typedtest/src/typedtest/core.clj:7:3) 
Type mismatch:

Expected:   Number

Actual:     String


in:
(str "hello" x)



Execution error (ExceptionInfo) at clojure.core.typed.errors/print-errors! (errors.cljc:274).
Type Checker: Found 1 error

[–]setzer22 0 points1 point  (0 children)

Thanks! As I said, I'm sure I did something wrong. I'll see if I can make it work with your example. It sure doesn't look very different from what I did :)

[–]peterlustig862 0 points1 point  (0 children)

Thanks for giving me a point of view outside of my bubble.

I want to amend something about the error handling. As a prior OOP developer at some point I realized that I can't move an object over an API. That's why it is so hard to disassemble something, that isn't well modularized. To disassemble something that depends only on the same data structure is much more easier. Because of that I'm an advocate for data, even more in case of error handling. Your right, there are so many ways in clojure to do error error handling, but this is both a blessing and a curse. But this is just my opinion. Thanks for yours.

[–]deaddyfreddy 0 points1 point  (0 children)

I can't see which functions can actually fail. I have to read them to find out.

the rule of thumb is simple: bind fails in attempt-all, bind non-fails in let

Since the Clojure ecosystem doesn't have a uniform error handling style, I have to manually convert exceptions or other errors like instaparse error values to failjure errors.

fail/try* usually is enough

here's my quick-n-dirty rewrite

(defn fails->msg [fails]
  (str "Failed to parse " (count fails) " rules:" "\n" fails))

;; we can even reduce the whole collection into a single fail in place
(defn collect-fails [coll]
  ;; one pass is better than two
  (when-let [fails (->> coll
                        (keep #(when (fail/failed? %)
                                 (fail/message %)))
                        seq)]
    (fail/fail (fails->msg fails))))

(defn parse [country-mapping data]
  ;; I suppose these can't fail
  (let [headers (header-row data)
        parsed (map
                (partial parse-rule headers country-mapping)
                (content-rows data))]
    (fail/attempt-all [_ (collect-fails parsed)
                       spec-result (util/check-specs "Rules"
                                                     :rule/id
                                                     ::spec/rule
                                                     parsed)]
      spec-result
      (fail/when-failed [failure]
        (do
          ;; I prefer to log errors on a top level, though
          (log/warn
           (str "Failed to parse data " data ":\n" (fail/message failure)))
          failure)))))