Fork me on GitHub
#clojure
<
2024-03-04
>
slipset08:03:27

I want to lift > • The error messages are terrible. I have no apologetics for this. from @jumar’s post about Kyle’s observations on working with Jepsen. Granted, I’ve been using Clojure for 10 years, and working with it daily for the last 7, but I can’t really say that the error messages are problematic, and in fact, I would really not want error messages on the Elm format, It’s a wall of “user-friendly” text which has to be read in order to understand the error. Certainly, there are error messages in Clojure which are less than helpful, eg

user> (map 1 (range 3))
Error printing return value (ClassCastException) at clojure.core/map$fn (core.clj:2770).
class java.lang.Long cannot be cast to class clojure.lang.IFn (java.lang.Long is in module java.base of loader 'bootstrap'; clojure.lang.IFn is in unnamed module of loader 'app')
user> 
But I’d argue that you should rather quickly be able to internalize these. Looking at this error though, the rather unhelpful jvm message that crops up
(java.lang.Long is in module java.base of loader 'bootstrap'; clojure.lang.IFn is in unnamed module of loader 'app')
which I believe cropped up in jdk9 brings me no value 🙂

4
1
valerauko09:03:48

I was always wondering if these argument errors would be easier to understand if you printed the value (and maybe through some compiler magic, the variable name that triggered it)

5
p-himik10:03:27

Not all values can be safely and reasonably printed. There are a lot of things in Java and Clojure errors that are reasonably understandable and often even useful to experienced developers. But often those or other things can get in the way of the actual information. Which affects both experienced and newbie devs.

p-himik10:03:04

With that being said, pointing out the exact location or at least printing the name of the "bad" thing would indeed be fantastic. No clue whether it's feasible.

p-himik10:03:23

An example from Python 3.11 (surprisingly, only works when executed via a file - interactively working with the interpreter doesn't show it):

def f(a, b, c):
    return a() + b() + c()


def g():
    return f(1, 2, 3)


g()
Running it:
Traceback (most recent call last):
  File "/home/p-himik/Downloads/test.py", line 9, in <module>
    g()
  File "/home/p-himik/Downloads/test.py", line 6, in g
    return f(1, 2, 3)
           ^^^^^^^^^^
  File "/home/p-himik/Downloads/test.py", line 2, in f
    return a() + b() + c()
           ^^^
TypeError: 'int' object is not callable
The whole error message has exactly what's needed to understand what's going on, where, and why. But I've stopped using Python years ago so can't easily confirm whether such errors are that helpful only in toy examples or also in some complex real-life code.

cjohansen10:03:39

I don't really know what Elm's messages look like, but messages don't have to be long to be helpful. This particular example should have looked something like this:

user> (map 1 (range 3))

1 is not a function in `(map 1 (range 3))`
Expected clojure.lang.IFn, got java.lang.Long
(I was too slow and @U2FRKM4TW beat me to it with a real-world example... Oh well, I can still dream)

p-himik10:03:09

Now let's take the error in the OP (I now I'm preaching to the choir here, but it can still be an interesting exercise). To us, it's pretty much obvious what's going on. What isn't obvious is where exactly it happens (the line number is nice, but they are sometimes wrong and there's no column information). Now, let's try to forget everything except Clojure-the-language to be able to better put ourselves in the shoes of a newcomer that's just read the reference or some guide. The following points are written as if by such an imagined newcomer: • Error printing return value - so the error is with the printing itself, huh. What if I don't print it? Cool, def'ing it works. Oh, it explodes later when I try to use it. [At this point, the newcomer has already spent tens of minutes trying to understand what's going on] • (ClassCastException) - seems clear enough, something was casted to something incompatible • at clojure.core/map$fn (core.clj:2770) - what's map$fn? It's not documented anywhere. All I can find are other error reports. [At this point, the dev will probably be able to figure out what it is, but it requires extensive searching and reading] • class java.lang.Long cannot be cast to class clojure.lang.IFn - alright, so this is probably where that ClassCastException comes from. And java.lang.Long seems clear [if the dev knows at least what "longs" are in programming]. But what is clojure.lang.IFn? Let me check - the docs say: "`IFn` provides complete access to invoking any of Clojure's http://clojure.github.io/clojure/s. You can also access any other library written in Clojure, after adding either its source or compiled form to the classpath." I don't see how it could be relevant to what I'm doing. The reference mentions it only in the "Calling Clojure From Java" section, but I'm not using any Java! Maybe what follows will shed some light? • (java.lang.Long is in module java.base of loader 'bootstrap'; clojure.lang.IFn is in unnamed module of loader 'app') - alright, this is complete gibberish, I give up.

valerauko10:03:13

> Not all values can be safely and reasonably printed. True, but it's better to try if possible, than to make the developer guess and debug for extended time to figure it out

valerauko10:03:05

I personally really appreciate the location markers in rust/python, so even just that could be a huge help

1
p-himik10:03:32

> better to try if possible It really isn't. It's trivial to make the program hang or ruin the current state of the terminal or make some file system changes by just trying to print something. The only option is to have a set of safe classes instances of which can be printed. Everything else will be printed as some #object[class ...].

cjohansen10:03:30

There are lots of values you can safely print, so doing that would help in probably most cases

valerauko10:03:31

Emphasis on the "if possible". Try a toString and give up if even that fails

p-himik10:03:54

> Try a toString That can hang or side-effect.

p-himik10:03:09

Just call (str (range)).

valerauko11:03:48

Damn I think there was a baby in that (range)

vemv11:03:55

The value could be tap ped perhaps

p-himik11:03:44

That would be more useful, but in a very limited manner. But that all rests on an assumption that running arbitrary code right at the point where an exception happens is possible at all. I myself have no idea whether JVM has such an ability. Clojure has in principle, but it requires the compiler wrapping every single form in (try ... (catch Throwable t ...)).

👍 1
vemv11:03:33

#C03KZ3XT0CF style tracing might be close enough! It doesn't "catch everything", but as I understand it, it instruments the Clojure compiler/runtime so you can see the values flowing

p-himik11:03:00

Maybe. But that should probably be an opt-in since it would change how Clojure performs. And at this point, might as well just use FlowStorm. :) (Assuming it does make exceptions for user-friendly - I have no idea).

vemv11:03:00

Sure, I intended to simply suggest using FS. I'm aware there's been misc efforts to instrument clojure.core from the outside, with mixed results

Noah Bogart12:03:48

I think your description is good, p-himik, but i don’t think it captures the full story. To start, when the long is a variable and the range is a variable, it’s much harder to find the problem location unless you’ve internalized map. It’s also harder to find if there are multiple things on a line, such as multiple maps or filtering.

p-himik13:03:11

Yeah, my intention wasn't to capture the full story, just to provide an example. The full story is much vaster, there's all sorts of errors and conditions.

👍 1
Noah Bogart15:03:30

Architecture question: I have a linting library (#C04SCGV2ATX) like clj-kondo or eastwood that is run with clojure -M:splint [paths] . The built-in rules are written in plain Clojure and globally registered (like clojure.spec defs), so loading a file containing a rule loads the rule automatically. I'd like to support custom rules, which would be plain Clojure code in some namespace local to the current working directory. I don't want to support exporting library-specific rules which frees me from worrying about security in dependencies. Given all that, would it make more sense to accept a list of namespace symbols to require or paths to load to load the custom rules?

borkdude15:03:41

I'd avoid load if you want to keep bb and clj-kondo happy ;)

👍 1
Noah Bogart15:03:41

good point about bb. i don't know that clj-kondo would break here, as the usage of the custom rules is entirely hidden, so there'd be one line in splint that says something like (doseq [path paths] (load path)) and in the custom rules, they'd just say (defrule style/my-custom-rule ...) and that's that

Noah Bogart15:03:39

given that I'd like to keep compatibility with bb, i'll probably go with require then. thanks, that's helpful

gtbono20:03:53

is there any way to print all arguments of a function? something like JS arguments keyword in Clojure?

emccue20:03:38

If you have a var holding a fn, and that var was made using defn, then the metadata of the var will hold stuff

Alex Miller (Clojure team)20:03:40

there are various tracing libs that can give you some variant of that if you want it

emccue20:03:53

(defn s [a b])
=> #'dev.mccue.template-processor/s
(meta #'s)
=>
{:arglists ([a b]),
 :line 1,
 :column 1,
 :file "/private/var/folders/cr/wpw7hgnx0jj9m33g28rtbpzc0000gn/T/form-init17864110765273705935.clj",
 :name s,
 :ns #object[clojure.lang.Namespace 0x4d144f08 "dev.mccue.template-processor"]}

Alex Miller (Clojure team)20:03:54

do you mean parameters or arguments?

emccue20:03:06

but nothing on the function object itself

Alex Miller (Clojure team)20:03:26

that above is parameters, but I read the question as arguments

gtbono20:03:07

@U064X3EF3 which libraries?

dpsutton20:03:43

(ns show-trace
  (:require [clojure.tools.trace :as trace]))
nil
show-trace=> (defn foo [x] (if (zero? x) x (+ x (foo (dec x)))))
#'show-trace/foo
show-trace=> (trace/trace-ns 'show-trace)
nil
show-trace=> (foo 3)
TRACE t227592: (show-trace/foo 3)
TRACE t227593: | (show-trace/foo 2)
TRACE t227594: | | (show-trace/foo 1)
TRACE t227595: | | | (show-trace/foo 0)
TRACE t227595: | | | => 0
TRACE t227594: | | => 1
TRACE t227593: | => 3
TRACE t227592: => 6
6
show-trace=> 

emccue20:03:34

Preview of the thing i'm doodling on, translating the design of Java's new string template feature into a macro

(let [ages [1 2 3]
      name "bob"]
  (<< sqlvec "SELECT *
              FROM person
              WHERE name = ~{name} AND age IN ~{ages}"))
=> ["SELECT *\nFROM person\nWHERE name = ? AND age IN (?,?,?)" "bob" 1 2 3]
I'm not sure if even I would use it without IDE syntax highlighting on the embedded language and usage detection on the variables, but its a fun experiment

🆒 2