Semantic Clojure Formatting
metaredux.com
metaredux.com
I think there is an important - but often ignored - argument against "hanging indentation", and that is that it makes the indentation of a possibly large number of lines depend on the length of the identifier or expression that the arguments are applied to. This can result in larger than necessary diffs when refactoring, leading to unnecessary conflicts during integration. Furthermore, unless you have tooling that is aware of this indentation style, code modifications may require a lot of manual and tedious re-indentation.
The first example code block is followed by the text:
> I hope it’s obvious that the main difference is that “semantic indentation” differentiates between macros with body forms and regular function/macro invocations.
But in the example code block the two "macros" examples were formatted identically in 'fixed' and 'semantic' styles. The only one that was different is "function call spanning two lines". So the stated point is far from obvious.
In Clojure you can define your own control structures, and the way to do that is using macros. That’s why the article talks about macros.
Does that help?
I'm just not sure I actually understand how that distinction is supposed to be implemented by the "semantic indentation" style. The example in the article only shows how there is a difference between the styles for _function calls_ (calls to `filter`). But I suspect the example is just a bad one for illustrating the authors point.
In general though, I think hanging indentation is a bad idea for the reasons I stated above :).
Given an expression such as (foo bar baz), semantic indentation examines foo to determine whether it is a macro or a function, and produces two different indentations depending on the result.
Fixed indentation always produces the same result, regardless of whether foo is a macro or not.
The "semantic" in the name refers to the deeper analysis required: If you see a "defmacro" somewhere then you have to remember that it is a macro.
(I think actually, it's a bit more complicated. Macros can have multiple arguments, and one is often special. Look at the control structure "when": it receives a condition and multiple "statements", and when the condition is true all statements are executed, in order. And semantic indentation knows this. So you get this:
(when CONDITION
STMT-1
STMT-2)
But when you put the condition on an extra line, you get this: (when
CONDITION
STMT-1
STMT-2)
Fixed indentation has no idea and produces the following hard-to-read thing instead: (when
CONDITION
STMT-1
STMT-2)Semantic versions of those could be interesting.
Consider, say,
for (i in [1, 2, 3]) {
x(i);
y(i);
z(i);
}
The indentation here very naturally demarcates between control structures and non-control code.Compared to similar lisp-y code:
(map
#(-> % x y z)
(range 3))
The indentation doesn't give the programmer any hints that map is actually a control structure fulfilling the same role as a for loop.Given that indentation is critical enough to be a major language feature of something like Python, lisps have a weakness here that isn't dealt with very often. This so-called 'semantic indenting' doesn't go far enough - programmers want much more of it if Python and C-like languages are any indicator. I think there is a very desirable equilibrium here; something where the indentation changes based on the semantic intention of the function. I don't want all my Clojure functions to indent the same way, map/reduce/recur/let/etc are special.
The equivalent of the first in Clojure would be (assuming it's js):
(doseq [i (range 3)]
(x i)
(y i)
(z i))
And the equivalent of the Clojure in the second example in js (assuming we had range()) would be: range(3).map(i=>z(y(x(i))))
If you're using a map as a for loop in js you wouldn't have much indication of that either. And no idea how to properly indent these applications.Like this perhaps?
range(3).map(
i=>
z(
y(
x(i))))
IMO single line is better with x,y,z. But perhaps with longer function names that's what you would do.Maybe think of it this way:
xxxxx {
xx
xx
xx {
xx
xx
}
}
vs (xxx
xx
(xx xx xx xx)
xx)
I can make a pretty safe guess that the first example is implementing something not worse than O(n^2) before accounting for what the individual calls do, or maybe there is some conditional branching. I can make some guesses about how the code works with probably fair accuracy. That would survive even if I took away the {} braces.In the second example there is not a lot that can be deduced. Whether that matters to you influences whether you are happy coding in a lisp.
I prefer the lisps, but there are less hints from the shape of the code.
In Clojure if you see a macro (will be highlighted as a different color) followed by something in square brackets you'll know what's up. The little extra syntax Clojure introduced is quite a big game changer to readability vs other lisps.
The problem is we can introduce new macros that may behave differently. You don't have that problem in languages without macros. But if you did have macros, brackets and indentation would lose signal too. Conversely if we didn't have user macros in Clojure the standard convention would be a 100% reliable signal too.
I mean, I agree in general that people may find C-like languages more readable, but these examples seem rather poor.
I can see why some people would find this more readable
if (a || (b && (c + 10 == 11))) {
doSomething();
}
than this (though I still believe this is mostly a cultural difference) (if (or a (and b (= (+ c 10) 11)))
(do-something))
But this is about extra syntax not indentation. You can indent both in many different ways.If anything, given the extra syntax, the js snippet has more possible indentations in this case.
First your examples are using different constructs, as well as different languages. It's perfectly reasonable and "lispy" to write a for loop like your first example, e.g. (this is pretty much the same as the first example in https://docs.racket-lang.org/reference/for.html )
(for ((i '(1 2 3)))
(x i)
(y i)
(z i))
Likewise Lisps don't have a monopoly on factoring common functionality out into helper functions like 'map', e.g. a C-like equivalent to your second example is not at all unusual: map(
seq(x, y, z),
range(3))
(I've replaced the symbolic name '->' with a made up non-symbolic name 'seq', to do the same job of applying multiple functions to an argument).Secondly you bring up "off-side rule" indentation, like Python, without mentioning I-expressions or their equivalents. These can be used by all Lisps (and anything else based on s-expressions, like DSSSL, since they're trivially convertable to/from s-expressions. Some Lisps also have native support (e.g. they're standardised in Scheme https://srfi.schemers.org/srfi-49/srfi-49.html ).
> map/reduce/recur/let/etc are special
I agree for 'let', and it already is a "special form" in most Lisps. I disagree for 'map' and 'reduce', since they are ordinary function calls that are not special in any way (that's part of the appeal for functional programming and macros: that we're not stuck with a finite number of first-order "control structures"). I've not encountered "recur" before, I assume it's a Clojure workaround for JVM tail-call deficiencies? A quick Google shows that it's also a "special form", like 'let'.
It's only alluded to in this article, but Clojure has a semi-community-standardised metadata format [0] for indicating how a function should be indented. It isn't very impressive, and I'm not saying this is a solved problem because it isn't. I've never seen well indented lisp code in the way that Python is excellent. But the technical aspects of how to solve it are implemented, the answer is 'add semantic metadata to the functions, have a Well Known Standard for indenting code semantically'.
There is no reason in principle that Clojure couldn't have Python-good indenting except nobody seems to have any good ideas for how to format code usefully. Probably a style breakthrough waiting to happen.
((foo() || bar())
&& baz() && (xyzzy ||
plop) &&
blah
versus: (and (or (foo)
(bar))
(baz)
(or xyzzy
plop)
blah)
You're barking up completely the wrong tree here in your search for Lisp problems.For Lisp, you have a simple algorithm with a few cases in it for indenting, which works equally well if you've broken every token out into a separate line, or just have a few line breaks.
There are no concerns like does this punctuator or operator "cuddle" into the previous line, or start the next line? Fewer aspects are a matter of taste, leading to better consistency from one project to the next.
Let's compare the coding style of the TXR Lisp standard library to randomly selected code written not only in completely different projects by different people, but in different dialects and languages:
TXR Lisp stdlib: http://www.kylheku.com/cgit/txr/tree/share/txr/stdlib/tagbod...
Clojure core: https://github.com/clojure/clojure/blob/master/src/clj/cloju...
GUIX internals (Scheme): https://git.savannah.gnu.org/cgit/guix.git/tree/guix/git.scm
SBCL compiler code (Common Lisp): https://github.com/sbcl/sbcl/blob/master/src/compiler/dce.li...
By golly, it all looks the same, formatting-wise, wherever I look!
I could throw any of that code into a stock installation of Vim with no plugins, and easily maintain its style with the built-in Lisp support. That Vim could be an old version from like 1999.
On the other hand, the Clojure kids seem to reinvent programming badly while maligning other Lisps (see blog post), so maybe they do have a massive problem with trivial things like indentation.
The standard could have been bigger and more comprehensive. But it’s just a non-problem most of the time. Threading is de facto standard and extremely well understood in its interactions with Common Lisp as defined.
Common Lisp is an extremely capable language to get things done cleanly. It just does things in a way that simply feels different from later languages.
If you want to talk about bolted on features, look no further than the plethora of Scheme libraries.
If your answer is performance or compatibility with 1980s operating systems or lisp implementations, then sure that’s a reason for the design but it doesn’t make the design cohesive, complete and composable.
I think CL is a good language despite this rather than because of it and I would suggest that your claim of it working “surprisingly well” supports that.
> Why can’t equality operators be extended to user-defined types?
That's a loaded question where a bad answer is worse than no answer.
I have taken a crack at it by inventing a concept called "equality substitution".
Equality for a new user-defined type is defined using a one-argument method, which takes an instance of that type itself. When an object of that type is compared with something using equal, then that method is called, and the return value is used instead; that is then iterated on. If an object has no such method, then its identity is used: the opposite argument must be that same object.
Thus, custom equality works by substituting some ordinary object: hence, "equality substitution".
For instance some employee object may have an equal function which returns the employee ID (an integer). Then employee object whose ID is 42, is equal to the integer 42, and therefore any other employee object whose ID is 42, or any other non-employee object also which returns 42.
To make it safer, the function could return a cons instead like (employee . 42), where the type symbol appears in the car. That cons is then equal only to another cons which has employee in car and 42 in cdr.
Equality substitution neatly solves the problem of hashing also. The hash able implementation uses the equality substitute of an object as the key: that's the value it hashes, and that's the value it compares.
The programmer need not define a custom hashing function, which is an error-prone, low-level task that we would not like to foist upon application programmers.
Now, not everyone will find it appealing that (equal non-cons-object '(employee . 42)) can yield true. They might say, if you believe that no answer is better than a bad answer, why on Earth did you do such a thing?
> Why pathnames?
Portability of code among very diverse historic operating systems, installations of which are no longer in deployment.
Pathnames in CL may be a pain in the but, but they are arguably a better abstraction than raw strings.
Mainly the thing that is wrong with CL pathnames is that the spec leaves certain behaviors implementation-defined in such a way that two people writing a CL implementation on POSIX, say, will end up with different behaviors. There is more than one way that POSIX path strings can correspond to pathnames objects. Oh, like for instance in foo.tar.gz, is "gz" the :type? Or is "tar.gz" the :type? That creates ironic situations whereby we can't port a Lisp program to the one single plaform using two implementations of the language, such that the behavior is identical!
A new revision of ANSI CL could solve this: it could specify that on Microsoft Windows, the correspondence between pathname objects and strings is such and such, and on POSIX such and such, and so forth.
We like our multiparadigmness, thank you very much. That includes being able to mutate things. We also like that the language is stable, and that means no changes to the spec, and that means it won't cover everything. That's OK, because our spec covers more than enough. We have less stable libraries and implementation-specific interfaces to cover the rest. We are still pretty conservative with those, too.
Clojure doesn't have a spec at all (it has documentation for the single implementation; it seems to have another implementation which is even more a toy since no real Clojure programs can run on it, because JVM is part of Clojure, really...) and once things are documented they either stick forever or break. I wouldn't say Clojure has warts; it has massive oozing injuries that keep on spreading indefinitely.
So yeah, we'd rather live with the warts from the 90s, the 80s, the 70s, and the 60s. We like our tradition, and we like our programs not breaking. We have a stable base that we can build 2020, 2030, 2040 stuff on.
I can take source code from the eighties and nineties (say from CMU AI archive) and, in all likelihood, with a few modifications make it respect the ANSI specification and have it run in 2020 on pretty much any CL implementation. This code is several times older than Clojure. I have a lot of code from 15 years ago to this day that works flawlessly on pretty much any CL implementation. Granted I also have some implementation-specific code and code that would suffer in efficiency and that would hit implementation limits.
I use mainly two CL implementations (SBCL, ECL) that serve purposes that are quite different. I know applications where CLISP was chosen because of its size and mostly-portable bytecode. People who care about Appleware tend to like CCL for its support on that platform. Some companies prefer the more enterprisey LispWorks/Allegro. These implementations are all meaningfully different. They all strive to conform to the specification. CLISP and Allegro have nonconforming modes as well, which not many people care about.
> Granted I also have some implementation-specific code and code that would suffer in efficiency and that would hit implementation limits.
That's my point. I mean that the Spec make you think it's always 100% compatible, but there are actually some small differences, and it's not always compatible or performing similarly.
Clojure just makes that reality explicit. Here are alternate implementations, here's what they support and where they differ, and don't trust that any implementation details might be exactly the same.
And for CL, a lot of choosing an alternate implementation is about finding one that supports the platform you target. But in Clojure, that's already handled by an intermediate host, like JVM, JS, or CLR. And so a lot of the meaningful differences between CL implementations arn't meaningful in Clojure, instead they'd be lifted to choosing which JVM you want to use, instead of which Clojure implementation you want to use. And it turns out the JVM is incredibly portable across platforms and does have a Spec, and so does indirectly Clojure.
So when you take it practically, the choice of implementation for me is about reach. And there are some places where CL reaches further then Clojure for now, specifically around embedded, IOT, lower level and such. There's also places where it doesn't reach as far as the JVM or JS hosts can give you. And yes, you can use say ABCL, but it's not as simple as copy/pasting your code to it. Part of wanting the reach of the JVM is to tap into its ecosystem of libraries and frameworks, and as soon as you do that, your ABCL code is stuck to only working on ABCL, and is no longer portable to any other CL host, same as Clojure.
I'd reframe the problem here, instead of saying CL is portable and has a Spec and Clojure doesn't. I'd say what exactly are you trying to achieve?
Do you want to run code written for x86 Windows on ARM64 FreeBSD? Well Clojure code is portable on both using the same implementation but a different JVM. And CL code can be portable as well if switching to a different CL compiler implementation with support for ARM64 FreeBSD.
Do you want to use JS libraries in a Common Lisp project and target the browser and NodeJS? Well, I'm actually not sure if CL has any way to do that, but assume it does, great you can do it, but most likely not have it be portable. Similarly with Clojure you have ClojureScript for that.
It is true that Clojure doesn't have a spec, but it is very stable. I saw some very minor breaking changes in Clojure, and most code just works after a update (including very big projects). Clojure is very conservative in general, and this is something that Rich says as a explicit design goal.
BTW, are you referring to ClojureScript? Because ClojureScript is far from a toy language, it works well and we use in production. Sure it have some warts, but they're mostly workable and while I am not a fan, I am not a fan of frontend in general so this is probably part of the problem.
The differences honestly are often pretty similar to what some CL implementations differ in: host interop and multi-threading/concurrency.
The biggest difference is that Clojure standard library doesn't wrap all host APIs, so there's a bigger reliance on direct host interop. That's the main thing. And because the hosts themselves don't all share APIs, the interop specific code needs to be written specifically to the host.
It's a similar story for multi-threading and concurrency, some hosts don't support it at all, or support a very different version like Actors only, so in those cases the Clojure features related to them are dropped or altered to match the host.
You can still provide multi-host libraries, by making use of reader conditionals you can have the code fork at the points where it needs to be specialized for each host. That in turn can be used portably afterwards on whatever hosts the library would have supported.
That's a claim that would need to be substantiated first. My experience is that typical Clojure is quite descent actually.
Nope, someone wrote an article arguing for fixed formatting and Bozhidar Batsov decided to reply, but pretty much what he said is what is accepted by Clojure community anyway.
Given their simple syntax, one of the lisps seem to be the best poised to drive this change.
Also Dhall, a configuration language, generates hashes for semantics so if you refactor something you can check that the hash is unchanged to know that the function is unchanged. https://dhall-lang.org/
But I think that's fine. The diff tool should use the most specific diff mode that it knows is safe, and fall back to the "free diff" of tree structured data when working on an unknown language.
Outside (apparently) clojurespace there has never been any bigger disagreement whatsoever regarding this.
The article mentions this. But why was there ever a discussion? The hoards of non-lispers going to clojure?
See for example:
https://dspace.mit.edu/bitstream/handle/1721.1/6503/AIM-1102...
XP, A Common Lisp Pretty Printing System, 1989 by Richard C. Waters
Interlisp dealt with code formatting for its support of true structure editing.
> They specify a couple core forms that have a body, which is indented "semantically" and the rest is treated like functions where new lines are indented to the first argument.
Your "they" looks like it refers to "examples", which is the first (actually, only) plural word that we encounter in a leftward scan from the pronoun.
I think it's actually supposed to refer to "lisp-aware editors".
Firstly, that is not quite right. There is no fixed core of forms that have a body, because user-defined macros extend that repertoire. You have to teach that to your editor, in the light of what projects you are editing. For instance, in Vi, the ":set lisp" mode has an associated "lispwords" variable which holds a list of the symbol names that are to be treated as operators that have a body rather than functions.
But the code in the Semantic Clojure Formatting page linked to by the submission is in fact exemplifying this formatting. It shows operators that have a body being indented, and function calls exhibiting argument alignment.
Can you cite an example of what you think is wrong?
The only exception I see is the "narrow format" for function calls. But that's not an invention from the Clojure world. I've resorted to that narrow format myself, in code that is deeply nested and hitting the maximum column limit of the surrounding coding convention.
(deeply
(nested |___|___|
......................... (aargh |__|___|__
too |___|___| brick wall
cramped |_|___|__
here |___|___|
I don't like it very much; I prefer to refactor the code to avoid it, if possible. But if you put a line break immediately after the function name, then that is realy the only sensible indentation you can follow.Emacs Lisp has an entire form for controlling macro indenting like that.
https://www.gnu.org/software/emacs/manual/html_node/elisp/In...
Say if I as a macro writer could specify semantically what the macro arguments are. if I were to invent the let form, I would have (name binding-form body...) And have the editor properly indenting the body 2 spaces in (or whatever the editor specified).
it would remove the little manual indenting I have to do myself (mostly because I haven't told Emacs how to indent some rarely seen forms).
It's interesting that the creator of Rubocop isn't that bullish on Prettier, because my company literally uses Rubocop to replicate a Prettier-like experience [1]. Rubocop makes a totally decent Prettier replacement, with the right rules. Another advantage of using a separate opinionated formatting tool is that you avoid issues with a diversity of editors which never quite have the same auto-indentation to mess up code diffs.
1: https://flexport.engineering/approximating-prettier-for-ruby...
I use it extensively and it's both easy to use and super flexible (for the rare cases when you do need that).
Here's a link to the indent spec, if you want to read more: https://docs.cider.mx/cider/indent_spec.html
Compare this with traditional formatters, which you can run on each single file individually, this is much more complex and also harder to parallelise.
If the program has syntax errors, nothing can happen, how would it know how to format it self if it is not correct?
Perhaps the approach is very different in lisp (and smalltalk which I have some experience with), there is no parsing of the program from the outside - or - if you take that approach and treat the s-expr as dead text you have to sort of reimplement the parser (or the system) in order to have some understanding of it, which in my mind seems very inefficient.
You don't want to be doing anything of the sort. That is a misconception. The representation of the Lisp program in memory is not the source code. Lisp is not homoiconic according to the original definition by Douglas McIlroy, which is that programs are stored in memory in the original representation entered by the programmer.
Some Lisps keep only a compiled version of the code in memory.
Even those that keep the structural source code in memory (e.g. in support of the ed function in Common Lisp or something like it), it will not be faithful to the original. For example, semicolon comments are stripped away by the reader, and any read-time processing is performed. For instance, Lisps that process the backquote syntax at read time will not preserve the original.
I've also never heard of a modern Lisp implementation in which a global variable definition like (defvar foo (+ 1 2)) retains the unevaluated (+ 1 2) somewhere.
How integrated Lisp environments work is that there are editing buffers with code, and code is re-read from those buffers when edited, into the same running image. Some of the buffers may contain "throwaway" calculations, like interactive testing, and some contain files that are persisted (and version controlled). The user interface doesn't necessarily run in the same image; it could be connected to a remote image (exemplified by users working with Emacs to interact with an image over Slime).