I hope I stay active in the industry long enough for people with influence to start talking about Human Factors as they apply to development tools. Ten years ago I thought that might be right around now, but today I'd still say ten years from now. I might be in my fusion powered self-driving car waiting for that boat to come in.
When I'm digging through a large body of code looking for subtle bugs, I want the code to be boring but not bland. By that I mean, yes, all of the bits should be obvious, because I'm having to contend with the cartesian product of all of the bits. But if everything is self-similar top to bottom, there are no landmarks. It becomes very easy to get 'lost' in the code and have trouble telling if the next candidate for debugging is 'up', 'down' or sideways in the call stack.
Fractals are really cool to look at, but they're murder for navigation purposes.
>Fractals are really cool to look at, but they're murder for navigation purposes.
Are they? They imply that the structure is self-similar, which is a good trait for a structure, and makes it easy to read it at any level and get what's going on.
That's what trees are, lists of lists are, strings of characters are, etc.
>But if everything is self-similar top to bottom, there are no landmarks.
The specific functions called at each level are the landmarks.
What specific functions? That's my point. If you go all in on recursive design, all the functions, variables, and object names are the same all the way up and down your graph. There are no specific functions. It's all grey goo.
I cannot speak for the others here, but with languages like say JavaScript, the symbols usually represent something. {...} will usually represent a block of code. "[...]" will usually represent an array-like index.
With Lisp you don't have such visual cues; you have to read the function name and perform a mental translation (lookup) of that function name to "find" a purpose in order to know what the "parenthesized unit" is. Thus, it's more mental steps to compute the general meaning of the code.
One is mentally searching (mapping) on name, not visual appearance.
Lisp fans seem to perform this lookup faster than average. Whether it's because they've been doing it for so long or they have an inborn knack is unknown. It would make a fascinating area of research.
I tried to get the hang of name-based fast recognition, but was progressing too slow for my comfort.
Lisp code usually has a complex tree like indentation & layout. Indentation is provided as a standard service by editors and by Lisp itself. See the function PPRINT as the interface to the pretty printer, which does layout and indentation of source code.
We look for visual tree patterns. For example the LET special form:
LET
BINDINGS
BODY
or more detailed
LET
VAR1 VALUE1
VAR2 VALUE2
...
BODYFORM1
BODYFORM2
...
The list of binding pairs is another common pattern.
There is a small number of tree patterns which are used in the core operators of Lisp. Once you've learned them, reading Lisp is much easier than most people think.
All indentation does is tell you that there is a hierarchy of some sort. The fact something is one level deeper than another still doesn't tell me generally what it does, because that depends on what the parent(s) does. And it's not a difference maker because "regular" language can also use indents.
Further, the coder decides the indentation, and I'm not convinced it's consistent enough. In my example, a curly brace is a curly brace regardless of a coder's preference. It's enforced by the rules the language, not the coder.
Another thing I'd like to point out is that languages like JavaScript provide two levels of common abstraction. Using curly braces, parenthesis, and square brackets as an example, you spot them and immediately know the forest-level category something is: code block, function call, or array/structure index.
With Lisp, all you are guaranteed have is the function name (first parameter), which you have to do a mental name-to-purpose lookup, which could involve thousands of names. Splitting the lookup into levels improves the mental lookup performance, at least in my head.
It also helps one quickly know to ignore something. For example, if I'm looking for a code block, I know I can probably ignore array indexes, and vice versa. It's forest-level exclusion, which is hard to do with single-level lookups because the list is too long: it has to contain the general category of each name. It's extra mental accounting.
Hard-wiring in big-picture categories into the syntax allows quickly making forest-level reading decisions, and which individual coders generally can't change.
When it comes to team-level read-ability, consistency usually trumps abstraction and many other things.
Maybe Lisp can do something similar with "let", bindings, and body, but then it starts to resemble "regular" languages, along with their drawbacks, which is typically less abstraction ability. Consistency "spanks the cowboys", both the good cowboys and the bad cowboys. But at least you know what you have, and can estimate and plan accordingly.
With lisp you stop thinking about syntax rules anymore (because the program is in a very simple form) so you can focus purely on semantics. Some things, like you say, are too overloaded in Clisp, for example:
That isn't how that defun would normally be formatted, though. Lisp knows that the 'body' part starts with the 3rd argument (the same way Clojure knows it starts with the second), so it indents the lambda list farther right (if it doesn't fit on the first line, otherwise it would go there).
For my eyes, [] aren't distinct enough from () to make the second style preferable. I'd rather have indentation to set it apart.
I just made them consistent. It certainly helps me visually. Also a list implies you intend to evaluate it by executing a function. A vector implies you do not intend to call a function.
Yeah, it's definitely a taste thing, but as the first style isn't used in Lisp, comparing its readability doesn't really make sense. Indentation is important to reading Lisp. Indenting it in an odd way makes it harder to parse. It's a bit like giving
def
foo(x)
bar(x)
end
as an example of Ruby syntax being overly homogeneous.
A Lisp programmer reads those structural patterns, not the delimiters.
Lisp programming is more about thinking of trees of code and their possible manipulation - even independent of a visual notation and especially independent of the exact delimiter used.
The delimiters are in shape recognition much less important than the shape itself.
"A Lisp programmer" ... Sheesh, I am a lisp programmer, man, and I assure you I know what trees of code independent of visual notation is. You're making a point that doesn't need to be made here. It's this kind of phrasing that really turns off people from the lisp community.
I wrote it that way so it's easier for non-lisp programmers to compare with what they're more used to as well.
> All indentation does is tell you that there is a hierarchy of some sort.
As I said, it's not just indented, but also layouted. As a Lisp developer I see a LET and then know that a binding+body structural pattern is following. binding+body and variations of those is used in a bunch of operators.
That's a simple sequence:
1) identify the operator on the left
2) it determines the layout pattern
3) visually apply pattern recognition based on the layout pattern and classify the blocks
Lisp is actually quite well to read, but you have to learn the basic structural patterns for a few days.
The biggest hurdles are mental blocks and prior exposition to other types of syntax systems.
> the coder decides the indentation
He doesn't. Lisp does it. There are indentation rules provided by the development environment. If Lisp does the layout, it uses a complex layout system to adjust the code to the available horizontal space, taking into account the constructs to layout. That's why Lisp developers use these indentation and layout tools for decades.
Lisp code itself is fully insensitive to formatting. Tools (editor, Lisp, ...) do the indenting and sometimes also the layouting for the user.
> Using curly braces, parenthesis, and square brackets as an example, you spot them and immediately know the forest-level category something is: code block, function call, or array/structure index.
Lisp uses symbolic names for that. People are extremely good at reading names. Since Lisp names are always at the beginning of a form, there is no guess/backtracking needed to find out the most top-level form. Where as with languages with infix operators I would need to parse the whole thing, finding out the operators and select the one with the highest precedence.
> With Lisp, all you are guaranteed have is the function name (first parameter), which you have to do a mental name-to-purpose lookup, which could involve thousands of names. Splitting the lookup into levels improves the mental lookup performance, at least in my head.
Lisp has not just functions, but also macros and special operators. Function calls have a fixed syntax and there is not much to understand.
Special operators and macros usually signal with their names the kind of structure and thus the layout expected to follow. For example there are a bunch of macros which begin with WITH- . Those signal this pattern
This is interesting, but not something I've really struggled with in Clojure. I generally try to write small functions, at a single level of abstraction, and if I feel there's too much complexity I'll extract part of it out.
It's true that this then results in lots of names. But that's also how I write code in every other language - 90% of reasoning about code is through named variables and functions, and compositions thereof. I'm struggling to picture how this becomes an issue, unless you genuinely don't use abstractions in code at all.
In particular I’m thinking of architectural astronauts who get excited about a “system” they want to build (or god forbid, already have built) where “everything is a foo”. The hallmarks of these systems include: lots of very self-similar code - often recursive, very low inclusion of domain nouns and verbs, and instead substituting a new (impoverished) domain that is full of vague nouns and verbs (like “node”, “execute” or “handle”). In the worst cases, different parts of the code use a separate definition of the vague nouns.
Such people would (and at least one case that I’ve seen, have) rush to call their system homoiconic. I think because they’re more interested in looking clever than being helpful.
Not only is such code hard to follow, but as one smart person once put it, any time your code uses different concepts than those from the problem domain, there’s a place where impedance mismatches live. Those look like bugs to your users, and are often the hardest to fix.
As the other responder guessed, this is a part about lisps that I’m not overfond of. It’s easier for such frippery to expand to the entire system.
The alternative to self-similarity at different scales has already been tested and more or less rejected: it's DSLs of the kind Lisp and Smalltalk let you create. The idea being that you build a high level language out of abstractions and it levers up the language.
The problem is that a custom language so built is harder for newcomers to the project to get to grips with. If it's a super common problem, and the solution gets popular, it might work out - see e.g. Rails with its DSLs for migrations, routes, etc. But that's a small fraction of problems. Hired developers don't really want to learn you custom thing, too, as it reduces their market value.
How was that idea rejected? Building languages out of abstractions is almost all of what you do when programming. When you create a bunch of functions and group them in a module, you've created a piece of language. An abstraction layer is a language that things above are coded in.
Lisp/Smalltalk DSLs only expand your capabilities here to syntactic abstraction / code generation. But the overall principle is the same.
When I'm digging through a large body of code looking for subtle bugs, I want the code to be boring but not bland. By that I mean, yes, all of the bits should be obvious, because I'm having to contend with the cartesian product of all of the bits. But if everything is self-similar top to bottom, there are no landmarks. It becomes very easy to get 'lost' in the code and have trouble telling if the next candidate for debugging is 'up', 'down' or sideways in the call stack.
Fractals are really cool to look at, but they're murder for navigation purposes.