dev-tooling

flowthing 2023-09-29T07:50:19.828719Z

So I've been using clojure.pprint/pprint for my pretty-printing needs so far, but it's really quite slow. I can't use Fipp because I don't want to take any dependencies (and because of e.g. https://github.com/brandonbloom/fipp/issues/37), so I thought I'd have a go at making my own pretty-printer. What I have so far is dozens, sometimes hundreds of times faster than clojure.pprint/pprint, and 12x-15x faster than fipp.edn/pprint at Fipp's own benchmark. (It also allocates 10-25x fewer bytes than clojure.pprint/pprint or fipp.edn/pprint.) The implementation (https://github.com/eerohele/tab/blob/b5f1c0dd86349d05184d59746a017d9ed27e852d/src/tab/impl/pprint.clj) is ~200 lines of code. There are some benchmark results here: https://github.com/eerohele/tab/actions/runs/6348406766/job/17244962062#step:7:365 One tradeoff is that it's not customizable like clojure.pprint or Fipp, but that's not something I really need. Anyway, I figured I'd throw this out there in case anyone needs something like this, or if anyone has any ideas for improving the current impl.

1
pez 2023-09-29T09:22:04.187129Z

This is great! I’ll take a look and see if it’s something Calva can make use for. Anything in there that might not work in ClojureScript?

flowthing 2023-09-29T09:37:47.105979Z

It depends. I've only ever used JVM-hosted ClojureScript, and I pretty-print ClojureScript evaluation results on the JVM side, so this should work there just fine. It won't work with self-hosted ClojureScript as is, but it shouldn't be too difficult to adapt for that, too, I think. I don't currently use ClojureScript myself, so I don't have an incentive to look into that much at the moment. 🙂

👍 1
2023-09-29T12:46:30.169359Z

nice codebase also, thanks for sharing!

flowthing 2023-09-29T13:00:20.704339Z

Thanks!

cfleming 2023-09-29T21:25:55.240609Z

What are the other tradeoffs here? One thing fipp does well is trying to print within a set margin, is that possible here? Are there differences in how that’s handled?

flowthing 2023-09-30T05:51:17.833899Z

Good questions! The most visible difference to clojure.pprint/pprint, I think, is the way reader macros are printed in some cases. tab.impl.pprint/pprint prints them the same way as fipp.edn/print:

user=> (tab.impl.pprint/pprint #'map)
#'clojure.core/map
nil
user=> (clojure.pprint/pprint #'map)
#'clojure.core/map
nil
user=> (fipp.edn/pprint #'map)
#'clojure.core/map
nil
user=> (tab.impl.pprint/pprint '#'map)
(var map)
nil
user=> (clojure.pprint/pprint '#'map)
#'map
nil
user=> (fipp.edn/pprint '#'map)
(var map)
nil
I think this accounts for most cases where tab.impl.pprint/pprint prints differently than clojure.pprint/pprint.

flowthing 2023-09-30T06:14:24.428579Z

Fipp doesn't always stay within the margin. For example:

user=> (fipp.edn/pprint {[] [-1000000000000000000000000000000000000000000000000000000000000000N]} {:width 72})
{[] [-1000000000000000000000000000000000000000000000000000000000000000N]}
user=> (tab.impl.pprint/pprint {[] [-1000000000000000000000000000000000000000000000000000000000000000N]} {:max-width 72})
{[]
 [-1000000000000000000000000000000000000000000000000000000000000000N]}
nil
I don't know of any cases where tab.impl.pprint/pprint blows past the margin.

flowthing 2023-09-30T06:18:52.467919Z

Other than those things, I'm sure there are tradeoffs I'm not yet aware of.

flowthing 2023-09-30T08:40:21.965819Z

I actually did not know that clojure.pprint also supports formatting code via (clojure.pprint/with-pprint-dispatch clojure.pprint/code-dispatch ...). 🙂 That tab.impl.pprint obviously doesn't.

flowthing 2023-10-02T07:57:07.575979Z

I have. Here's a diff comparing the output of Fipp to the output of tab.impl.pprint printing the sources of all clojure.core vars: https://gist.github.com/eerohele/82d2ac3719fbd7de8d36ac154a7829bd The main differences are that Fipp prints (var foo) and tab.impl.pprint now prints #'foo, like clojure.pprint, as mentioned above (would be easy to make configurable). The other differences are cases where Fipp either prints past the margin or doesn't print until the margin even though there's space to do so, as well as Fipp having a different preference for printing map entries with multi-line values (all of the {:inline ...} cases in the diff).

cfleming 2023-10-02T10:22:07.767939Z

Very interesting, thank you. Cursive currently uses fipp for its pretty printing, and I’d love to be using something which is more understandable and doesn’t go through the intermediate document formatting object step. The differences there are interesting, did you have a minimum width set? e.g. I’m unsure why for the eduction var, pp makes the choice to insert a newline after the :indent and before the (fn. It looks like in that case, fipp decides to defer the line break to later, whereas pp has a longer line including the 'clojure.core/unchecked_int_remainder later on.

flowthing 2023-10-02T11:12:02.279549Z

pp doesn't have know concept of minimum width. I don't know about Fipp -- I used Fipp's :width argument for that diff. Fipp doesn't have a lot of docstrings, so I don't know all the options Fipp supports. pp inserts a newline after :indent because it determines that it cannot print the entire (fn ...) expression that follows on the same line as :indent without any line breaks. That's also how clojure.pprint works. Looks like Fipp makes the determination on a line-by-line basis somehow. pp prints the expression with 'clojure.core/unchecked_int_remainder on one line simply because it fits within the 72 character limit. I think Fipp inserts a line break because it has fewer characters available to print the expression: Fipp prints quote instead of ', and it has the (fn ...) expression starts on the same line as :indent.

cfleming 2023-10-02T11:13:28.647279Z

Yeah, fipp clearly has less available for the later line because it doesn’t break the line near the start like pp does.

cfleming 2023-10-02T11:13:52.206349Z

Aesthetically I think I like fipp’s choice, but obviously they’re basically equivalent.

cfleming 2023-10-02T11:14:57.536149Z

BTW this is one of my all time favourite articles, in case you haven’t already read it: https://journal.stuffwithstuff.com/2015/09/08/the-hardest-program-ive-ever-written/

flowthing 2023-10-02T11:15:00.867609Z

Sure. I could definitely look into whether I could make pp work like Fipp in this regard, but I don't really want to make the algorithm much more complicated.

flowthing 2023-10-02T11:16:16.094909Z

I haven't read it yet, but I've seen it recommended elsewhere before. Thanks, I'll definitely check it out. :)

cfleming 2023-10-02T11:16:46.704889Z

Yeah, I think fipp is considerably more complex, and the simplicity is really attractive from an ongoing maintenance point of view. I’ve never had to customise fipp, but I wouldn’t know where to start if I had to.

flowthing 2023-10-02T11:16:53.594489Z

I think pretty-printing is way easier than formatting proper, though.

cfleming 2023-10-02T11:17:22.314759Z

Sure, and it all depends how strict you want to be about the line length limitation, too.

flowthing 2023-10-02T11:17:34.750219Z

And pp very much relies on the formattee being a Lisp, which definitely helps. 🙂

flowthing 2023-10-02T11:18:00.039349Z

Yep.

flowthing 2023-10-02T11:18:12.862069Z

I stumbled upon this comment earlier in Fipp's issues:

flowthing 2023-10-02T11:18:18.561389Z

> Use the Fipp engine, but a custom Edn printer. This is the approach that @cursive-ide chose, as they output IntelliJ display objects instead of text.

flowthing 2023-10-02T11:19:26.577619Z

So I don't know anything about IntelliJ display objects, obviously, but I pp has so little code that I imagine it wouldn't be prohibitively difficult to adapt the algorithm to output those instead of strings, but not sure, of course. 🙂

cfleming 2023-10-02T11:19:36.047379Z

Yes, that’s right. fipp has two parts, the object is parsed and the layout decided, and the output of that is a series of formatting objects, Then in fipp proper those are printed, and in Cursive they’re printed using IntelliJ’s output functions (and highlighted etc).

flowthing 2023-10-02T11:19:50.719269Z

I see, interesting. 👍

cfleming 2023-10-02T11:20:54.805029Z

I wouldn’t call them display objects really, it’s more like “append this text to this editor, but in this style”, e.g. fg/bg colour, bold/italic, other highlighting like errors, hyperlinks etc.

flowthing 2023-10-02T11:21:20.120429Z

Ah, I understand.

cfleming 2023-10-02T11:21:28.112839Z

It’s complicated because Cursive also interprets ANSI escapes, which are a PITA.

flowthing 2023-10-02T11:22:39.365219Z

Oh yeah, I've steered clear of those thus far. 🙂

cfleming 2023-10-02T11:27:35.445999Z

Reading through those issues, it looks like you originally ditched fipp due to lack of print-method support. But print-method is often just incompatible with pretty-printing. Are you planning to support it, or did you just decide that support for it wasn’t something you needed?

flowthing 2023-10-02T11:46:59.790389Z

pp uses print-method for everything except colls, basically.

flowthing 2023-10-02T11:48:50.347489Z

(And pretty-printing is only really relevant for colls, I think.)

flowthing 2023-10-02T12:08:58.505069Z

pp doesn't have the problem I referred to in that commit message where I ditched Fipp, for example:

user=> (cpp/pprint #time/date "2023-10-02")
#time/date "2023-10-02"
nil
user=> (prn #time/date "2023-10-02")
#time/date "2023-10-02"
nil
user=> (pp/pprint #time/date "2023-10-02")
#time/date "2023-10-02"
nil
user=> (fipp/pprint #time/date "2023-10-02")
#object[java.time.LocalDate "0x1375388b" "2023-10-02"]
nil
But it's entirely possible print-method has pitfalls I don't know about. I'd be very interested in hearing about them if you know of any. :)

bozhidar 2023-10-02T12:59:19.390959Z

I’ll take a look at your implementation as well. Might be something we can bundle with Orchard for people who want to stay light on deps.

flowthing 2023-10-02T19:09:04.240629Z

I think I'll end up pulling this thing out into a single-namespace, no-dependency lib so that folks can use it either by pulling a dep or by copy-pasting the namespace into their own codebase, so you might want to hold off on trying it out until I get that done.

flowthing 2023-10-01T18:32:36.241949Z

FWIW, I did a bit more work on this... tab.impl.pprint now prints reader macros the same way as clojure.pprint and supports *print-namespace-maps*. It now supports all of the same clojure.core/*print-* options as clojure.pprint (that is, all of them except *print-dup*). Through generative testing and comparing the output of tab.impl.pprint and clojure.pprint printing clojure.core var sources, I'm now fairly confident that the only place where tab.impl.pprint prints differently than clojure.pprint are cases where clojure.pprint doesn't make full use of the line width even though it could (as well as one meaningless difference in where each decide to insert a line break to avoid blowing past the margin).

cfleming 2023-10-02T01:26:38.920569Z

Have you also compared the output to fipp?