I don't know enough Clojure to really understand this argument. I am a bit confused by the example. It appears that the only difference between "semantic" and "fixed" indentation is that the former allows "hanging indentation" of the arguments, i.e. aligning everything to match the indentation of the first argument when that was not preceded by a newline but placed immediately after the identifier of the function being called. I don't understand how that has anything to do with macros as it is claimed in the article, but that is probably due to my lack of Clojure knowledge.
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 cases where hanging indentation is used are plain function calls. The places where the other kind of indentation is used are similar to control structures in other languages. In C like languages you can tell by the curly braces that it’s a control structure (if, while,...). In Clojure they all use parentheses.
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.
Yes, I think I understand that. The idea is to make a visual distinction between a function call and a macro invocation, as the latter is used as a mechanism to provide control structures that are usually built-in in Algol-like languages.
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 :).
> I'm just not sure I actually understand how that distinction is supposed to be implemented by the "semantic indentation" style.
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:
I almost always edit Clojure in emacs with aggressive-indent-mode enabled. Effectively, what this means is that the buffer is always formatted: every keypress triggers reindentation of the entire buffer and emacs “knows” about these semantic indentation rules. With this setup, the rules are pretty trivial to enforce and other environments, such as Calva or Cursive, can be configured to approximate this pretty closely. I dislike this style in other languages, but in lisps, the two-dimensional layout it creates is very useful for quickly scanning code and understanding its structure.
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.
While I agree with the argument that it’s better to let the semantics drive the formatting, I wish we could get over formatting discussions already, with the codebase recording the structure, and the displayed format being a matter of editor configuration. Tabs, spaces, linewidths, whatever... knock yourself out. A bonus would be if vcs patches/diffs are semantically aware. Has this ever been tried seriously?
Given their simple syntax, one of the lisps seem to be the best poised to drive this change.
There are various tools for this: I believe one of the interns at Jane Street wrote a tool to diff s-expressions, and similar tools exist for lisp source code. Ultimately, the issue is that tooling is mostly made to be programming-language independent, which makes it hard for this sort of tooling to succeed
I think making a tool like that language independent is impossible. You have to start by making it a diff tool just for tree structured data (diff tools today are special cases of this as they operate on list based data). You then need to extend it to work on specific languages by diffing "modulo equations", e.g. in Haskell two top-level definitions are allowed to commute, so `x = e; y = f` should be considered identical to `y = f; x = e`.
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.
I looked into using tree-sitter as a bar for this once: parse code into an AST of sorts and then use the ast to define the diff boundaries. Didn’t think through all the problems, though
You might be interested in Unison, where you edit code in text but let the manager/runtime handle the storage. It changes how you need to work with VCS quite a lot. https://www.unisonweb.org/
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/
Indenting styles is, humorously, a massive problem for the lisps.
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.
Lisp programmers have SLIME or equivalent, rainbow-parens and the like. They trade syntactic cues for semantic editing capabilities, which is why indentation/syntactic cues are less of a big deal in lisp communities and at the same time one the reasons that keep lisp in the shadows of the programming world.
When you write list, you can't know which of the structures are control and which are not (and why would you in the first place?). Because macroses can give any semantics, including control one, to the provided list. You think of the structures as lists, and care about narrative "all children of the list are at the same indentation level". That's where I find this "semantic formatting" a useful thing.
I think your point might go to issues implementing a solution technically? Just in case it does...
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.
Massive problem? That's news to me. But what do I know, I've only been writing Lisp for more than a decade.
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.
I think the Clojure people tend to be pretty thoughtful about language design and doing things in ways that will be functional and composable. Common Lisp is a language with a lot of warts where many features were tacked on but not properly integrated with everything else. It also suffers from the concerns and technology of the 1980s (a big focus on mutability, no multithreading in the spec, the pathnames library, no immutable maps, to name a few.) Scheme is minimal and that can be an advantage but Clojure has different goals to scheme
I think the Clojure people tend to produce unreadable mess, which tells me the design of their language is down in the gutter. Common Lisp is a language with a lot of warts, sure, but typical Common Lisp code is way, way, way more tasteful than typical Clojure code.
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.
If true, that is a point for Clojure. It keeps the inexperienced on a distinct path where some day, when they're battle-worn and wise and had enough of it, they may find it was a road to Lisp after all.
> 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...)
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.
I was thinking about ClojureCLR. While my argument could be applied to ClojureScript, I did not think it was fair because ClojureScript calls itself a (proper) subset of Clojure, which is fine and no one expects it to run actual Clojure programs. Again, I apologize for undue harshness.
Lots of Clojure code is portable between Clojure, Clojure CLR and ClojureScript.
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.
I've found in practice, Clojure's different implementations are actually way more useful then Common Lisp. In CL, you can in theory take code from ABCL, and then run it over SBCL, but the spec is actually a lie, and not all code will be portable, and most implementation have their own slight deviation and additions to the Spec. And in CL, each implementation is basically the same with a few differences, and thus very few reasons to use one or the other. Where as in Clojure, there's good reasons to go from Clojure Java to ClojureScript to Clojure CLR, to Babashka, to Clojerl, to Ferret, etc. Each time you switch the target platform you get a Clojure that is well adapted to the new platform host. That has made it much more practical I've found in practice. It's actually because there are enough host specific differences that in the end I find it more useful.
I guess our experience with regards to CL differs. A lot. The spec is lie? Implementations are all the same? To me it sounds like you're talking about a different language.
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.
> and, in all likelihood, with a few modifications make it respect the ANSI specification and have it run in 2020
> 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.
I really consider your comment to be FUD about Common Lisp. The language is not so aesthetic in many places, but the language was very certainly thoughtfully designed and iterated on. Features by and large work surprisingly well together to form a cohesive language. I don’t agree about things being “tacked on”, which is voluminously evidenced in the discussions about each and every feature.
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.
I’ve written a lot of Common Lisp and I while there was obviously a lot of thought that went into its design, I wouldn’t call it cohesive. Why are streams not CLOS objects (in the spec; I know about gray-streams)? Why can’t equality operators be extended to user-defined types? Why does the language even need so many equality operators and specialised functions like sbref? Why have both setf function names and a load of functions like rplca? Why is a setf function name invalid as the car of a form, even though only expressions (apart from atoms and lambdas) are disallowed? Why can’t the user make hash tables work for their own data-types? Why aren’t structs a first-class part of the object system like classes (unless you have the MOP)? Why can’t you have anonymous classes even though you can have anonymous functions and objects whose class was replaced with a new definition? Why can’t the user extend the sequence functions like map with their own sequences? Why are arrays so custom compared to the rest of the object system? Why pathnames? Why do so many things have bad names (e.g. getf and putf vs setf, set and get and setq, ...)
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.
I've written a not-so-small-anymore Lisp implementation, and wouldn't call it cohesive. Even a one-person design won't be cohesive without a laser focus on cohesivity, which guides every design decision, and which sacrifices compatibility with anything existing, also.
> 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.
> massive problem with trivial things like indentation.
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.
You are correct. If it helps you can imagine that the functions have side effects and always evaluate to their input.
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.
Hmmm, well, so if we're talking purely about indentation wrt control structures, I can see the point, maybe. But if we're talking about the ability to eyeball what's going on, I'm not sure...
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.
Your example is a bit apples-to-oranges, in a few ways.
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'.
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:
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.
Someone really ought to do a lisp where code actually is data and amongst other things save people from having to write articles arguing about where to semi-manually put their invisible-to-the-language-artisanal-whitespace and how attempting to fully automate the process tramples the human spirit.
Why can you not just give macros enough meta-data so the reader-formatter knows what kind of formatting the form desire? Provided your lisp have macros of course...
CIDER, which the author of this blogpost maintains, has support for such metadata, under the style/indent key. The hope, when it was created, was that more editors would adopt this, but after years it's not caught on.
I use it extensively and it's both easy to use and super flexible (for the rare cases when you do need that).
One problem with this is that formatting now becomes a multi-file thing. You have now have to figure out what names actually refer to, which depends on building a project model to correctly resolve imports, etc....
Compare this with traditional formatters, which you can run on each single file individually, this is much more complex and also harder to parallelise.
I am not sure if I understand what you mean with multi-file and why it matters. As a lisp, the program is already in memory, I can interact with it through the repl. I am sure I can ask it to format itself?
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).
The semantic formatting seems better to me, but I feel like I'll get used to almost any reasonable formatting. If you spend enough time with a consistently formatted codebase, your eye will be drawn to the important parts when you review code. In a tiny way, you develop "expertise" with the formatting, and when you look at other code there is an ever-so-subtle friction to understanding it. Manually reindenting code after editing or refactoring it is also pretty annoying, though of course that's the instinct that "code is meant to be read" is intended to argue against.
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.
I would call the fixed indentation just plain wrong in the examples. No lisp-aware editor will indent it like that. 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.
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?
Actually the Lisp tradition has provided extensive support for formatting code. Common Lisp has this built in, in the form of pretty printing. Various IDEs have some degree of formatting rule support built-in.
No. That is a macro specifying how it should be indented, which is a great idea.
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).
Your comment is a bit unclear. Can you give an example of what you think no Lisp-aware editor will produce?
> 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.
58 comments
[ 3.5 ms ] story [ 114 ms ] threadI 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.
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:
But when you put the condition on an extra line, you get this: Fixed indentation has no idea and produces the following hard-to-read thing instead: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.
Semantic versions of those could be interesting.
Given their simple syntax, one of the lisps seem to be the best poised to drive this change.
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.
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/
Consider, say,
The indentation here very naturally demarcates between control structures and non-control code.Compared to similar lisp-y code:
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.
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.
[0] https://docs.cider.mx/cider/indent_spec.html
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.
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.
That's a claim that would need to be substantiated first. My experience is that typical Clojure is quite descent actually.
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.
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.
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.
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.
The equivalent of the first in Clojure would be (assuming it's js):
And the equivalent of the Clojure in the second example in js (assuming we had range()) would be: 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?
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:
vs 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
than this (though I still believe this is mostly a cultural difference) 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 )
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: (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'.
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.
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).
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...
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.
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).
> 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.
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.