Introducing Standard Clojure Style

Introducing Standard Clojure Style

tl;dr

  • there is a new Clojure source code formatter
  • it has no config options and follows simple formatting rules
  • it is very fast
  • written in ~4,000 lines of JavaScript with no dependencies
  • it is stable enough for most projects
  • try it out and report bugs!

Background and Context

Back in 2018 Daniel Compton created a Clojureverse thread proposing to build a common formatter for Clojure code similar to gofmt or JavaScript’s prettier. This thread generated a lot of useful discussion, but ultimately no single tool was created.

For the last year or so I have been working on Standard Clojure Style, which is my take on this problem. You can read more about the genesis of the project at Issue #1.

Current Status: ready for most projects

As of v0.11.0, I think the library is stable enough for most Clojure projects. If you have node.js installed on your system you can try it out with the following commands:

# go to a Clojure project directory
cd ~/my-clojure-project

# check out a clean git branch so you can revert any changes made by the tool
git checkout -b testing-standard-clj

# check your files
# NOTE: your directory names may be different, please adjust accordingly
npx @chrisoakman/standard-clojure-style check src-clj/ src-cljs/ test/

# format your files
npx @chrisoakman/standard-clojure-style fix src-clj/ src-cljs/ test/

# review the changes and report any bugs upstream

Please see the project README for more details about CLI options, installation, etc.

It would be helpful to run it on as many codebases as possible to collect bugs, edge cases, etc. Please create an issue on the repo if you run into something that breaks your codebase.

Many thanks to several volunteers who have already done this and reported bugs :pray:

Performance is a feature

Standard Clojure Style is fast. Some quick benchmarking on my 2023 MacBook Pro M3:

Results will vary of course, but it is doubtful that Standard Clojure Style will be a slow point in your development tooling.

Goals and Next Steps

It is not a goal of Standard Clojure Style to be “the one style that everyone has to use”.

Rather, the goal is to encourage community adoption of a common code style by offering easy-to-use tooling that works well enough and gets out of your way.

If you are currently using zprint, cljfmt, or another tool and are happy with the result, then Standard Clojure Style may not offer a compelling reason to change.

If you are currently using nothing, or starting a new project, then consider adopting Standard Clojure Style and see how it works for you / your team.

The initial implementation is written in JavaScript, but the code is written in such a way that it is designed to be easily portable between programming languages. This is inspired by how Parinfer works (see parinfer.py, parinfer-lua, etc), and I hope to port the Standard Clojure Style algorithm to multiple languages in the future once the implementation is stable.

The long-term goal of Standard Clojure Style is to “meet you where you are”: as a CLI tool, on your continuous integration build, in your editor, on the web, etc.

Upcoming Conj 2024 talk

I will be giving a lightning talk about Standard Clojure Style this Thursday at Clojure/conj 2024. Please come say hello if you will be at the Conj :grin:

Thanks for reading!
Chris Oakman


6 Likes

I like the initiative and it seems to work fine. One big nitpick though, since it affects almost all my source files.I ran this over the shadow-cljs src/main folder and ended up with

6 files formatted with Standard Clojure Style
174 files require formatting
Checked 180 files. [641.63ms]

It changes the ns form in pretty much all files, which is fine and I wouldn’t mind.

The other change it wants to do and would be a blocker for me, is where I place some closing parens. It always wants these as “tight” as possible, so always moving them into the place they are syntactically correct, with no whitespace inbetween.

I often use them structurally though. What I mean is that I’m adding a line break, and then place as many closing parens as needed to close that “block”. A “block” here might be a logical group of some statements, or just visually grouping conditionals or so.

Just a randomly picked example from the actual code:

(defn ui-page [build-id tab]
  (<< (page-header build-id tab)
      (case tab
        :runtimes (tab-runtimes build-id)
        :config (tab-config build-id)
        (tab-status build-id)
        )))

Just to be clear, it will format this as:

(defn ui-page [build-id tab]
  (<< (page-header build-id tab)
      (case tab
        :runtimes (tab-runtimes build-id)
        :config (tab-config build-id)
        (tab-status build-id))))

It might seem weird, arbitrary and inconstitent when going over the code. It basically is. I cannot really explain why or even when I do this, other than it is an aid for me visually and I would not want this to be changed.

I don’t know how you’d make a rule in a formatter for this, and that I should likely just accept it and move on. I already know that I won’t though, because frankly there is little upside. I guess maybe the formatter could “relax” a bit about closing parens if they are indented correctly .

1 Like

Here is a small playground using squint:

https://squint-cljs.github.io/squint/?src=KHJlcXVpcmUgJ1siaHR0cHM6Ly9lc20uc2gvQGNocmlzb2FrbWFuL3N0YW5kYXJkLWNsb2p1cmUtc3R5bGUkZGVmYXVsdCIgOmFzIGNsai1zdHlsZV0pCgooZGVmb25jZSBwcmUgKGRvdG8gKGpzL2RvY3VtZW50LmNyZWF0ZUVsZW1lbnQgInByZSIpCiAgICAgICAgICAgICAgIChzZXQhIC1pZCAiYXBwIikKICAgICAgICAgICAgICAgKGpzL2RvY3VtZW50LmJvZHkucHJlcGVuZCkpKQoKOzsgbm90IGluZGVudGVkIG9uIHB1cnBvc2UgdG8gc2VlIGlmIHN0YW5kYXJkLWNsb2p1cmUtc3R5bGUgaW5kZW50cyBpdDoKICAoKyAxIDIgMykKCihzZXQhIGpzL2FwcC5pbm5lclRleHQKICAoLT4gKGNsai1zdHlsZS9mb3JtYXQgKC0%2BIChqcy9lZGl0b3Iuc3RhdGUuZG9jLnRvU3RyaW5nKSAudHJpbSkpCiAgICA6b3V0KSk%3D

2 Likes

It would be interesting to see how the clojure-mode formatter (based on codemirror, lezer) performs. It’s also written in 100% JavaScript (well, ClojureScript, but also in squint and then compiled to JS). It’s also available on NPM.

I did a quick POC with clojure-mode:

(ns formatter
  (:require ["@nextjournal/clojure-mode" :refer [default_extensions]]
            ["@nextjournal/clojure-mode/extensions/formatting" :refer [format]]
            ["@codemirror/state" :as cm-state
             :refer [EditorState]]
            ["node:fs" :as fs]))

(defn push! [arr x]
  (.push arr x)
  arr)

(defn make-state [extensions doc]
  (.create EditorState
           #js{:doc doc
               :extensions extensions}))

(defn state-str [^js state]
  (let [doc (str (.-doc state))]
    doc))

(defn apply-f [extensions cmd doc]
  (let [state (make-state extensions doc)
        tr (cmd state)]
    (state-str (if tr (.-state tr) state))))

(def apply-f* (partial apply-f default_extensions))

(def input (-> js/process.argv (aget 2)
               (fs/readFileSync "utf-8")))

(time (apply-f* format input))

On clojure core.clj this takes about 1300ms on my machine. With Standard Clojure Style it’s about 2x as fast:

All 1 files formatted with Standard Clojure Style :+1: [587.09ms]

Btw, when formatting clojure/core.clj with Standard Clojure Style it breaks the code:

user=> (load-file "src/clj/clojure/core.clj")
Syntax error reading source at (src/clj/clojure/core.clj:8131:1).
EOF while reading, starting at line 5783

Standard Clojure Style pretty-prints ns forms “from scratch” every time. For the rest of the code it “adjusts” what is already there.

Issue #60 contains a pretty thorough discussion of this behavior. Standard Clojure Style supports using a , to “hold” the parens on the next line for those cases where the developer does not want to gather trailing parens as recommended by the Clojure Style Guide. A rich comment block at the end of a file is a common use case for this.

Just want to point out that this is exactly the sort of thing that creates small friction on PRs (“nit” comments). This can also be confusing to developers who are new to Clojure (when to do this? when not to?) and is incompatible with Parinfer users, who never have to think about this because their editor always manages closing parens for them.

This is not to suggest “you are wrong”, or “this is bad”, or even that you should stop doing this! Only that it is inconsistent style and can cause extra diffs in a codebase that is shared by multiple developers all using different editors and tooling.

My hope for Standard Clojure Style is to alleviate these small frictions for development teams who adopt Clojure.

1 Like

Good catch! This is fixed with v0.12.0

I’m not super fond of this; in places where I need a lot of requires I like to group things with whitespace, add comments, etc, and rewriting the ns form wrecks that.

Before:

(ns review.app
  "Entry point for the application."
  (:require [reagent.dom    :as dom]
            [re-frame.core  :as rf]

            [review.connect.views   :as connect-views]
            [review.view-mode.views :as view-mode-views]
            [review.db              :as db]
            [review.mw              :as mw]
            [review.views.loading   :as loading]

            ;; I don't directly reference any symbols in this, but it needs to be pulled
            ;; in somewhere so that it can register the :http-xhrio effect handler
            [day8.re-frame.http-fx]

            ;; And some of my own effect handlers
            [review.effects]
            [review.events]
            [review.subs]))

After:

(ns review.app
  "Entry point for the application."
  (:require
   ;; I don't directly reference any symbols in this, but it needs to be pulled
   ;; in somewhere so that it can register the :http-xhrio effect handler
   [day8.re-frame.http-fx]
   [re-frame.core :as rf]
   [reagent.dom :as dom]
   [review.connect.views :as connect-views]
   [review.db :as db]
   ;; And some of my own effect handlers
   [review.effects]
   [review.events]
   [review.mw :as mw]
   [review.subs]
   [review.view-mode.views :as view-mode-views]
   [review.views.loading :as loading]))

I don’t mind lowering the indentation level at all, but deleting the whitespace I’ve added for readability bugs me.

Edit: Oh, I didn’t even notice the sorting at first. I think I just flat out disagree with that rule. :joy:

I do this all the time w/ rich comment blocks:

(comment
  (lots ..)
  (of ..)
  (stuff ..)
  
  ;;
)

Using a comma instead of that empty comment is a reasonable tradeoff, I like it.

I do this in rich comment blocks after I saw someone on the internet doing it :wink:

(comment
(f …)
(f2 …)
*e)

1 Like

I will be giving a lightning talk about Standard Clojure Style at Clojure/conj 2024

Here is a 10-minute lightning talk introduction and demo of Standard Clojure Style: https://www.youtube.com/watch?v=VhjxvEabOX0

2 Likes