Fork me on GitHub
#clojure
<
2023-07-27
>
Noah Bogart14:07:41

Is it possible for the Clojure run time to know if it’s being run “as clojure” or being hooked into from existing Java code? I’ve been thinking a lot about clojure error messages recently and it feels like some of the issues come from having to treat all messages as potentially exposed in a Java app calling into clojure code, which is pretty limiting

2
p-himik14:07:41

Probably by analyzing the stack trace.

borkdude14:07:56

Also with the official clojure CLI there is a system property called clojure.basis

👍 2
borkdude14:07:03

Not with lein though

👍 2
dpsutton14:07:24

> ’ve been thinking a lot about clojure error messages recently and it feels like some of the issues come from having to treat all messages as potentially exposed in a Java app calling into clojure code, which is pretty limiting can you explain this a bit more? I’m not sure i follow this part

Noah Bogart14:07:31

in other languages, there's no hosted runtime, so they can have arbitrary error handling. they can create an error object with arbitrary data and then display that data however they want. For example, Rust's extremely verbose and detailed error messages with cute little lines and underlines. Java is not built for that kind of detailed information, just "basic" Throwables with a message and stack trace. Clojure has ExceptionInfo, but Java ignores the attached data. If the Clojure runtime were to know it's in a clojure-centric app, then when there's an error, it could potentially display different or better information than just the error messages it currently does.

Noah Bogart15:07:05

For example, the clojure compiler prints a nice message to *err* if a macro spec fails and throws an ExceptionInfo: "Syntax error macroexpanding some-ns/some-macro at (some-path.clj:1:2). ... - failed: (some-pred) at: [:opts] spec: :some-ns/some-spec/opts" That printing is handled by spec. However, if you try to deref a hashmap, you get a ClassCastException: "class clojure.lang.PersistentArrayMap cannot be cast to class java.util.concurrent.Future". That's not a super helpful message if you're looking at a complex call because it's coming straight from Java. But it could be nicer if there was a global exception hook that (when running as a clojure app) modified or replaced the java exception system to make these errors better. So (some-call 1 @foo bar) maybe still throws a ClassCastException (or maybe not idk), but it instead prints "Error dereferencing foo in call (some-call 1 @foo bar)), foo is not IDeref, it's an IPersistentMap." That processing and printing is handled by this new part of the Clojure runtime.

Noah Bogart15:07:29

I expect that because of the ability to use Clojure code within Java, you can't do this globally or by default, as Java expects things to work like java, not like clojure

p-himik15:07:20

Such a thing or something very similar was discussed before. I think the verdict was "it's the tooling's concern".

Noah Bogart15:07:08

Yeah, I just think that's a poor answer given the continued prominence of "clojure errors" in the year survey

p-himik15:07:31

It was also discussed, right in one of the threads about the survey. :) Shouldn't be hard to find.

Noah Bogart15:07:34

the spec message is great and i want more like it in clojure. i think being able to actually surface the offending form would do wonders

p-himik15:07:47

But maybe you have already seen that discussion, I dunno. There's also an Ask about it which you could vote on, I think.

Noah Bogart15:07:02

lol yep, i was in that thread. lead to a good conversation about how to get emacs/cider to show the 1.10 error messages and not show the full stack trace by default

kennytilton15:07:18

My naive take is based on my Common Lisp experience. CL compiles right to the metal, but when I hit an exception, the call stack is all CL functions. Perhaps that is not feasible with CLJ? 🤷

p-himik15:07:43

At least to some extent feasible but definitely isn't always desirable, so it should be an opt-in thing. And with the solution being in the tooling (which already exists), it's opt-in by default.

Noah Bogart15:07:50

Well, it half exists. The 1.10 error messages update did a lot of good work which I'm grateful for, but it feels like the java-based errors are still hard to deal with, which is where my current attention is.

kennytilton15:07:07

I guess I know what is meant by "it's the tooling's problem"; I am always impressed by Figwheel's presentation of exceptions. And once I got frustrated and filtered the call stack dump, discarding all the Java entries. Worked great.

Joshua Suskalo16:07:42

A lot of this particular problem isn't actually about error messages at all, but rather where errors are caught. A lot of decisions about how clojure code is written is with the garbage-in, garbage-out mentality, which is chosen to assist with making Clojure viable from a performance perspective. Being able to construct an error like the "foo is not IDeref, it is a map" requires you to do validation on arguments and return types, to keep form information, and a lot of other things. This could be accomplished, of course, but it comes with the tradeoff of either having a lot of aggressive assertions added into code, or by having the compiler generate a lot of catch clauses which add tokens to the stack which cause performance issues in their own right.

Joshua Suskalo16:07:45

I'm not saying this to discount the issue or possible solutions, but rather to say that a general-purpose solution built into the runtime would itself have a lot of implications and tradeoffs that would have to be considered, which strengthens the suggestion of saying "just have the tooling handle it". The trouble in my opinion is that the tooling doesn't get enough support given to it.

Joshua Suskalo16:07:01

One potential though that could be considered as a language-level consideration would be to add prepost maps to a lot of core clojure functions that are at the bottom of the stack, as long as they get turned off when you disable assertions. This might also need some small modification to the prepost map facility to enable hand-written error messages.

Joshua Suskalo16:07:27

Things like seq, deref, assoc, conj, calling an ifn (though that would have to be in the compiler not just in a prepost map), etc. could all have assertions that get removed when evaluated with assertions disabled, could all get better error messages this way and that could propagate up the whole stack to make things more debuggable.

Noah Bogart16:07:29

I think that's related to but slightly separate from what I'm thinking about. (AssertionErrors are also impenetrable: Assert failed: (pos? a) is meaningless if you are looking at the call site, not the definition.) Rereading the https://clojure.org/reference/repl_and_main#_error_printing reference page on http://clojure.org, I think what's missing is additional information when the phase is :execution. The other phases include all sorts of stuff, but (as one might expect) the execution phase doesn't have access to that info a lot of the time. I don't know a good solution at the moment but I feel like if that info could be added somehow, runtime error messages would be a lot more helpful or usable.

Joshua Suskalo17:07:12

This is why I was saying custom error messages being added to assertions would be necessary to fully fix this problem. That said, I think you have the right idea on direction there.

👍 2
jpmonettas17:07:51

Has the idea of creating a "dev mode" for the compiler been explored ? So the compiler can emit different byte code taking different trade-offs, like emitting a lot of extra byte code to improve error messages, that will not be there when compiling in "perf mode". It can even be tried on a fork of Clojure, since it is very easy to swap compilers, like I'm doing with ClojureStorm in FlowStorm.

Joshua Suskalo17:07:41

A primitive version of this already exists with the compiler flag to omit assertions, which is why I was talking about that direction.

jpmonettas17:07:15

yeah that is a similar thing, but if you define something like "dev mode" maybe you can go much further

Joshua Suskalo17:07:45

the fork idea is interesting, as you could just make an alias in your deps.edn which would require a different clojure "version" from a different group id or from git

jpmonettas17:07:36

yeah, that is how ClojureStorm works today, and it works nicely with deps.edn and lein. It even add some extra stuff to repl behavior

jpmonettas17:07:18

the nice thing with the fork is that it can be explored without much core team involvement and the stuff that works maybe can be merged back some day

respatialized16:07:50

Say I wanted to ensure that every implementation of a multimethod (or protocol) returned a map with required keys. Can I use spec to enforce a post-validation constraint on any implementations so that they all return data in the expected shape regardless of implementation details?

dpsutton16:07:11

put a function in front of the multimethod or protocol and do what you want to inputs and outputs of that function

2
dpsutton16:07:57

(defn foo [x] (s/valid? <spec> x) (let [ret (-foo x)] (s/valid? <return-spec x) x)) type stuff

2
respatialized16:07:43

That works, but not having the spec as metadata on the multimethod itself feels incomplete from an introspectability standpoint (not dissimilar to the common Clojure experience of diving through two or three facade namespaces to get to the actual code you want)

p-himik17:07:55

If that method is the only public interface to the multimethod then you get the very same introspection ability (metadata-wise, methods would still have to be called separately). And adding new methods can be done via a separate function, if you want. You don't really have other options here due to the nature of multimethods.

kenny20:07:37

Is there any basic tooling for executing clojure build tasks only when a set of files have changed? e.g., only run the uberjar task if any of the files in src or deps.edn have been modified, else noop

vemv20:07:51

like a watcher, or like Makefiles?

kenny20:07:55

make is a valid answer 🙂 I was hoping to just stick with the beautiful clojure cli interface though.

p-himik20:07:19

Should be possible with bb, see how modified-since is used here: https://book.babashka.org/#_dependencies_between_tasks

lread20:07:41

Ha @U2FRKM4TW I was just typing the same answer!

clojure-spin 2
seancorfield21:07:52

We rely on Polylith at work to detect namespace-to-namespace level changes and dependencies so we can run only the tests that are needed and build only the (uber) JAR files that are needed. But we're building nearly two dozen JARs from close to 200 subprojects at this point 🙂

seancorfield21:07:12

For a single artifact project with a single src folder and a single deps.edn file, is it even worth not building the JAR file when the only changes are not covered by those two? In several of my OSS projects, I have build.clj containing a ci task that runs all the tests and only builds the (library) JAR if the tests pass. Is that maybe the context you're trying to cover @U083D6HK9 -- where you run tests and only build the JAR if they pass?

kenny21:07:56

Fancy stuff 🙂

kenny21:07:06

bb does sound like a good option. Definitely worth evaluating. This is used in a non-clojure project, so I’d be inclined to require the absolute minimal set of cli dependencies possible to make integration smooth. Folks seem to have a distaste for installing things on their computers 😜

kenny21:07:13

And sort of, Sean. I’m integrating the compile+build of a Clojure project into a IaaC tool (e.g., Terraform). Each time that tool runs, it currently executes the clojure -T:build all command, which is quite fast, considering, but it runs every time, which is not necessary since the code almost never changes. I was hoping to optimize this process by skipping the clojure build if no clojure files or deps have changed.

seancorfield21:07:34

Gotcha... interesting use case there... so make is probably your best option here?

kenny21:07:49

I do accept that as a valid answer, haha. I was sort of hoping for something like this.

(defn all [_]
  (when (has-changes?)
   (do (clean nil) (prep nil) (uber nil))))

kwladyka22:07:02

I don’t know the use case, but probably I would choose solution outside of the clojure. This is typical use case for cicd tool, but if you need this during developing I would use some watcher tool.

practicalli-johnny00:07:06

I use a Makefile when building in Docker (multi-stage) image, caching deps and source code in separate layers to minimise downloads I hadn't considered optimising the Uberjar build, although I could configure the make task to check if src or resources have changed, but not sure if this would have a significant difference in my case https://practical.li/blog/posts/build-and-run-clojure-with-multistage-dockerfile/

vemv20:07:16

Do you like this gpt4-generated code? Docstring says what it does

(require '[ :as io])
(require '[clojure.tools.reader :as r])
(require '[clojure.tools.reader.reader-types :as rt])

(defn read-form-at
  "Reads a Clojure form from a file at a specific line and column."
  [file line column]
  (with-open [rdr (io/reader file)]
    (let [push-back-reader (rt/push-back-reader rdr)
          line-reader (rt/indexing-push-back-reader push-back-reader)]
      (loop []
        (when (< (rt/get-line-number line-reader) line)
          (rt/read-char line-reader)
          (recur))
        (while (< (rt/get-column-number line-reader) column)
          (rt/read-char line-reader)))
      (try
        (r/read push-back-reader)
        (catch Exception e
          (println "Could not read form at line" line "column" column)))))
It looks reasonable to me, I just don't know if there's an alternative that can be considered more idiomatic.

Darrick Wiebe20:07:45

Is there a reason you can't read the whole file? I'd probably prefer to read the whole thing, then use a zipper to iterate the resulting forms to find the best match based on the metadata the reader adds to them.

p-himik20:07:13

Won't it throw the "recur not in tail position" error?

Darrick Wiebe20:07:39

it definitely would.

vemv20:07:48

fixing gpt4 code is to be expected :)

Darrick Wiebe20:07:38

I think working code would be a good starting point for asking these types of questions. Otherwise you're kind of wasting everyone's time. Well, that's my opinion....

15
vemv21:07:31

Thanks for the reword, I removed my negative emoji Please consider this a high-level question. > > Is there a reason you can't read the whole file? I'd probably prefer to read the whole thing, then use a zipper to iterate the resulting forms to find the best match based on the metadata the reader adds to them. Because I have in advance the precise line and column where the form is known to be defined, so I don't have a reason to use more a more fuzzy approach

ghadi21:07:54

@U01D37REZHP agreed. Every time I look at something generated by GPT, I want my time back

😍 2
4
p-himik21:07:06

It also uses read-char in the line-based when, but could use read-line. That whole loop is not needed, should be another while and at a different level. And not sure why both a push back and an indexing push back reader are needed, seems like having only the latter would suffice. But if you fix all that, seems reasonable. And I'd argue that it's much more useful to try to write such code yourself even if one doesn't know the required pieces. Just finding out about those pieces is incredibly useful and can't be done by constantly asking ChatGPT for stuff.

dpsutton21:07:32

(source source) is one of my favorite things to type in a repl and kinda does what you are wanting already?

p-himik21:07:42

> Please consider this a high-level question. I agree with others and I'll add to it - high level questions should be asked with a low level of detail. If you're interested in the overall approach, you should ask about the approach and not about some random not working piece of code that gets half of the things wrong and makes it unclear what the actual approach is.

dpsutton21:07:44

(source clojure.repl/source-fn)

👀 2
p-himik21:07:37

Oh, that's a good one.

vemv21:07:21

A certain spectrum of people (not necessarily better or worse than others) can be asked fuzzy questions and give useful insights. Like Dan did You do not need to answer to questions that do not make you comfortable. Peace!

Darrick Wiebe21:07:23

I think it's up to the person requesting help to be considerate toward the group of people who are taking their time to be helpful. But hey, that's just me.

1
dpsutton21:07:09

one annoyance with the way source works is that it uses read. and thus cannot read some namespaced alias literals

p-himik21:07:47

@U11BV7MTK Heheh.

$ cat x.clj
(ns x)

(defn f [])

(do) (defn g [])

$ clj -Sdeps '{:paths ["."]}'
Clojure 1.11.1
user=> (require 'x)
nil
user=> (in-ns 'x)
#object[clojure.lang.Namespace 0x5f78de22 "x"]
x=> (clojure.repl/source f)
(defn f [])
nil
x=> (clojure.repl/source g)
(do)  ;; <--- Check this out.
nil

dpsutton21:07:50

yeah. implicitly relies on some conventions. guess it could be improved with the column info

dpsutton21:07:12

❯ cat f.clj
(ns f
  (:require [clojure.set :as set]))

(defn f [] ::set/keyword)

/tmp via ☕ v17.0.1 on ☁️  metabase-query
❯ clj -Sdeps '{:paths ["."]}'
Clojure 1.11.1
user=> (require 'f)
nil
user=> (source f/f)
Execution error at user/eval149 (REPL:1).
Invalid token: ::set/keyword

dpsutton21:07:18

this one bites me every now and then

borkdude21:07:03

If you have the start row/column + the end row/column, you don't even need a Clojure reader to get out the form

2
borkdude21:07:01

> one annoyance with the way source works is that it uses read. and thus cannot read some namespaced alias literals This is easy to fix, there's a JIRA issue for it

borkdude21:07:49

If you want to read forms as strings, as is, you can just use the start + end location or if you have only a start location, #CHB5Q2XUJ is also a good option

borkdude21:07:38

tools.reader also supports read+string to get back the string as it was processed

vemv22:07:40

For posterity's sake, here's what I got

(defn read-form-at
  "Reads a Clojure form from a file at a specific line and column."
  [{:keys [file line column]} ^Namespace current-ns]
  (let [column (or column 1)]
    (when-let [resource (io/resource file)]
      (with-open [reader (io/reader resource)]
        (let [push-back-reader (reader-types/push-back-reader reader)
              line-reader (reader-types/indexing-push-back-reader push-back-reader)]

          (while (< (reader-types/get-line-number line-reader) line)
            (reader-types/read-line line-reader))

          (while (< (reader-types/get-column-number line-reader) column)
            (reader-types/read-char line-reader))

          (binding [reader/*alias-map* (ns-aliases current-ns)
                    reader/*data-readers* *data-readers*
                    reader/*read-eval* false]
            (reader/read {:read-cond :allow
                          :feature #{:clj}}
                         push-back-reader)))))))
Sample call: (read-form-at (meta #'read-form-at) *ns*) Nothing too different from what I've done over the years, I just wish there was a handy helper (well, now I created a utils.reader ns) for what seemed to me like a quite common use case.

practicalli-johnny00:07:41

The doc-string in the original GPT post looks basically okay, I would rewrite every other line and form that GPT generated There are many idioms it's ignored and I don't consider what was generated as maintainable code