We need to talk about parentheses (2020)
andreyor.st
andreyor.st
This is why automatic indentation is important when working with Lisp. This is mentioned later in the article, but I consider it mandatory because of the multitude of problem this approach resolves.
For example, upon pasting the below snippet into emacs:
(let* ((foo 10)
(bar (+ foo 32))
(* foo bar))
The editor 1) notifies me that there is a missing paren somewhere, 2) automatically* reindents this code into: (let* ((foo 10)
(bar (+ foo 32))
(* foo bar))
At this moment, it's obvious that we have three variable bindings, one of them malformed, and the programmer can correct this by planting a missing paren in the proper place. I wrote about this topic at length in https://mov.im/blog/phoe%40movim.eu/cd3577f6-fb1d-45f5-b881-... before.* with aggressive-indent and electric-indent enabled in emacs, as soon as the missing paren is placed at the end
If different kinds of brackets had been used for every distinct purpose, then there would have been no need to count parentheses or be dependent on enforcing invariable indentation rules.
ALGOL 68 is another example of programming language with fully parenthesized non-ambiguous syntax, but it uses many kinds of different bracket pairs, which makes it much easier to read. While some of its bracket pairs, like if/fi, do/od, case/esac, have been rightfully mocked, they achieve well their purpose.
In a programming language there are many different purposes for which bracket pairs are needed, e.g. for delimiting sub-expressions in order to change the default order of evaluation, for delimiting function argument lists, for delimiting array index lists, for delimiting blocks, for delimiting function bodies, for delimiting record/structure component definitions, for delimiting the components of various program structures, like conditional expressions and loops, and so on.
For an automatic parser, the use of different kinds of brackets can only make its task easier, never harder, because in the worst case its lexical analysis could replace all kinds of brackets with a single kind, while for a human reader different kinds of brackets are always helpful.
Using many kinds of bracket pairs is not necessarily as verbose as in Algol 68, because Unicode offers enough bracket pairs to choose for all purposes.
In LISP all parentheses look the same, but a LISP programmer who does not recognize that the things delimited in those parentheses are of different natures, e.g. "(CAR ..." vs. "(COND ..." vs. "(QUOTE ..." and so on, cannot write high quality LISP programs.
Many years ago, I have written many rather big Scheme or LISP programs, because they have many advantages beyond the uniform parentheses. If I would use again a language of the LISP family, I would use a text preprocessor to pass the source to the compiler, which would allow me to use various kinds of bracket pairs, for instance Unicode mathematical angle brackets for COND, Unicode bag delimiters for loops, curly brackets for blocks (curly bracket is the Unicode term for braces), and so on.
(car (first a))
(cond ((> a 10) 1)
((> b 20) 2)
((> c 30) 3))
Above looks different for me. As a Lisp programmer I don't look for parentheses, but for patterns with a starting symbol. Remember: list forms typically begin with a symbol.> If different kinds of brackets had been used for every distinct purpose, then there would have been no need to count parentheses or be dependent on enforcing invariable indentation rules.
No Lisp developer counts parentheses. Indentation rules are used in many programming languages. Lisp applies these rules using tools, like many programming environments do.
Any takers?
Edit: Or you could use markup to make the text size smaller and smaller.
IIRC, I have seen once an experimental language which worked exactly like in your proposal, but it was long ago and I do not remember which was it.
One LISP system that I have used long ago had a different approach. In it, one closing square bracket could be used to close all non-closed parentheses preceding it.
That was Interlisp, originally making handling of punch cards easier. Nowadays it's not necessary, since the 70s/80s text editors on terminals support handling nested expressions.
let hello = dbg!{ dbg![ dbg!("world") ] };
So it feels like this could be a reasonable plugin?
Actual delimiters explicitly structure your code, and then automatic formatting confirms it, kind of like double-entry book-keeping.
For me, just a couple of days with lisp and parentheses made me wish every language had such simple explicit syntax, I really recoil at some language's special syntax, eg python list comprehension. Maybe I just hate python, lol.
To that end, the first () in a "let" should be of the forms (name value), where value can be a longer form, if needed.
To that end, the last (name value) in the list should almost always look like (name value)), as it is closing the first in the list, that is ((name value).
(let* ((foo 10)
(bar (+ foo 32)))
(* foo bar))
Or, to demonstrate the ugly "nailclip" style: (let* ((foo 10)
(bar (+ foo 32)))
(* foo bar)
)
Note that I moved a closing paren off the third line and onto the second one, to close the variable bindings in the LET* form. ~ let*
~ ~ foo 10
~ bar (+ foo 32)
~ * foo barSure, if you write C like it's 1995, it's not going to be very readable. You don't need to define variable at the beginning of a scope, and you can use {} to scope variables to tighter blocks. And functions can be small.
(Sometimes you need to keep variables for longer than you would like for cleanup reason, that's true.)
But in my domain I have seen countless one-line functions used in one place each, and it’s frustrating having to jump around the code (and add to my mental context stack*) rather than just putting some curly braces and a comment. Functions aren’t primarily meant as a hygiene mechanism. I really like the curly brace scoping in Rust.
I found my style partly by reading John Carmack and Casey Muratori’s coding philosophies and putting them to the test for myself.
* For a natural language analogy, consider the difference between parenthetical remarks and asterisks. You have to remember your place in the text.
IMHO that is a side effect of how we teach people how to program mixed in with orgs cargo culting acronyms and not the core concepts of ideas.
Want people in a large organization to ignore your ideas? Speak them out loud.
Want them to follow them as dogma? Write a blog post under a pseudonym.
I always try to write code like
fn foo() {
let foo = do_first_thing();
let bar = do_second_thing();
do_third_thing(foo, bar);
}whenever possible. Then you can see the abstract overview of what the code does at a glance, but you can drill down into the details if you need to.
I much prefer breaking problems down to the level where I can reason in terms of sets, algebras, and that sort of thing. It's much easier for my brain to rely on properties and the ability to substitute terms in equations than it is to reconstruct a giant state machine and execute it in my head. Much more so when we add concurrency to the mix.
At the end of the day though I don't think there's, "one true way."
An ability that is only valuable if it really is a procedure, some sequence of operations, rather than a function, some forming of a value. I'd agree that procedures should generally not be split up into subprocedures; a procedure has to be read from top to bottom, so factoring out subprocedures only makes it harder to understand. But a function can be understood without understanding the smaller functions that it calls, so breaking functions up into smaller functions - and breaking small functions out of procedures, where possible - makes it easier to understand.
Having a language that makes an explicit distinction between procedures and functions - in particular, where calling a procedure looks different from calling a function - helps a lot (e.g. Haskell).
Sometimes you need explicit scope for a bunch of things and the code would benefit from it. That doesn't justify having explicit scopes for everything all the time.
But still, the original post said that they have to deal with this:
> because I always see about 10 uninitialized variables at the start of each function, where half of those are used only once
That's bad!
Nothing in the middle of this code prevents using ‘a’ even though it isn’t relevant to ‘c’. And there’s no way to arrange it to change that.
def doD(b: Int, c: Int) = puts(b / c)
def doCD(b: Int) = getInt >>= {c => doD(b, c) >> puts(c)}
def doBCD(a: Int) = getInt >>= {tb => val b = tb+a; doCD(b) >> puts(b)}
getInt >>= {a => doBCD(a) >> puts(a)}But I never got into Haskell so I don’t really know.
I don't think so? If you write it as a "flat" block using do notation then anything from earlier in the block is accessible later in the block. But local functions are real functions with lexical scope, they don't get hoisted up to the containing scope or anything. (And FWIW that's Scala-style syntax, although the ideas are very Haskell-like)
Have you written much Prolog?
I was thinking about in Prolog terms and I think it’s the hardest paradigm to avoid the scoping problem in
How does that prevent 'a' from being in the scope where 'c' is defined? It is not clear to me what you mean by defining 'c' later. Later where? And how do we ensure that 'a' is not accessible there?
I think lex-lightning's comment has a point. We need to define 'a' and we need to define 'c' and then we need to print 'c' before printing 'a', so although 'c' does not depend on 'a', we still need to keep 'a' in scope because we cannot print it before we print 'c'.
Perhaps a convoluted solution to the problem posed in lex-lightning's comment would be something like this:
#include <stdio.h>
int getint() {
return 42;
}
int main()
{
int a = getint();
int b = getint() + a;
int c;
{
// Shadow 'a' here to make the outer 'a' inaccessible in this scope.
int *a = NULL;
(void) a;
c = getint();
}
int d = b / c;
printf("%d\n", d);
printf("%d\n", c);
printf("%d\n", b);
printf("%d\n", a);
return 0;
}
Output: $ cc -std=c99 -Wall -Wextra -pedantic foo.c && ./a.out
2
42
84
42One note: each variable only gets assigned to once, so FP won’t magic that away.
The side effects might have a different way of management in FP. But the IO will happen in the same order in reality anyway.
So I don’t think there’s a way out, but I honestly would be interested to see it.
I’d argue that even declaring ‘c’ is against the spirit though. To say nothing of the eldritch shadow cast there (no disrespect, I think we’re both enjoying playing around here)
Yes, I agree. My code example is meant to show the best we can possibly do to solve the problem in your comment and how contrived that solution is. The fact that this solution is contrived and diverges from the intended spirit of the problem only emphasizes the point of your comment.
That's why I am eager to understand what zelphirkalt really meant when they wrote, "the re-arrangement seems simple: define `c` later". It seems far from simple and, in fact, impossible if we want to avoid contrived solutions.
Again, none of this is saying that lisp is bad. I just think we can have nicer things and preserve the important semantic insights that lisp gives us.
Development environments with syntax support are common. These are not "external", they are nowadays "integrated". See the features of typical Java IDEs. Java uses tools to edit code, so does Lisp.
Which is why "best of both worlds" approaches work best in my opinion, no matter if it's "just" automatic indentation and paren-counting that leaves actual editing to humans, or more intrusive approaches like Parinfer (mentioned in the article) that automatically infer parenthesisation from code indentation and edit the code as appropriate.
Properly indented Lisp is easy to read (for me), and badly indented code can be misread even in languages like C++.
The same code which runs
CL-USER 10 > (let* ((foo 10)
(bar (+ foo 32)))
(* foo bar))
420
becomes data, when quoted: CL-USER 11 > (quote
(let* ((foo 10)
(bar (+ foo 32)))
(* foo bar)))
(LET* ((FOO 10) (BAR (+ FOO 32))) (* FOO BAR))
A typical Lisp function can then substitute all * symbols to + symbols: CL-USER 12 > (subst '+ '* '(LET* ((FOO 10) (BAR (+ FOO 32))) (* FOO BAR)))
(LET* ((FOO 10) (BAR (+ FOO 32))) (+ FOO BAR))
EVAL can execute lists: CL-USER 13 > (eval (subst '+ '* '(LET* ((FOO 10) (BAR (+ FOO 32))) (* FOO BAR))))
52
Thus the single main feature is that Lisp itself is an application of the List Processor. This feature then is user visible in read eval print loops, debuggers, inspectors, code formatters, source interpreters, code generators (-> macros), embedded languages, ... So Lisp developers loved it, because one would not need translators between user facing (infix) syntax and internal data structures (ASTs, ...). Thus early attempts for other syntax variants failed, because this duality between data and code has some kind of elegance. Something which for example impressed Alan Kay (of OOP / Smalltalk / Dynabook / ... fame), when he understood that something like a Lisp evaluator can be written in a few lines of Lisp code, processing lists, which is code. -> https://www.quora.com/What-did-Alan-Kay-mean-by-Lisp-is-the-...I do have to confess I have rarely taken huge advantage of this fact. But the idea is pretty easy to help look at code.
The shallow view allows in Lisp to provide new/different views of code. That's good and bad. Many transformations of code don't need a deep understanding, many do. In Lisp I can write (infix 3 + 1 * sin (x)), because an infix macro can provide its own interpretation, by transforming the code. But then I need to parse the code and find out what the things are: variables, literals, operators, calls, ...
For example when I want to substitute FOO with a call:
CL-USER 34 > (LET* ((FOO 10) (BAR (+ FOO 32)))
(symbol-macrolet ((foo (print 2)))
(* foo bar foo bar foo ; 1-3
(flet ((foo (a) (* foo a))) ; 5
(foo (* foo bar)))))) ; 4
2 ; 1
2 ; 2
2 ; 3
2 ; 4
2 ; 5
2370816
Here the symbol-macrolet knows which FOO to expand, and which not. There are five expansions in the code, which makes 2 printed five times. You can see that it works in nested code, too : it knows that some FOO are function names and those will not be expanded.Whereas a MACROLET knows, where an operator needs to be expanded:
CL-USER 38 > (LET* ((FOO 10) (BAR (+ FOO 32)))
(macrolet ((foo (a) `(print ,a)))
(* foo bar foo bar (foo bar) ; 1
(flet ((bar (a) (foo a))) ; 2
(bar (* foo bar))))))
42 ; 1
420 ; 2
3111696000This is a list:
(munich frankfurt dresden)
This is a list of lists ((münchen :state Bavaria)
(frankfurt :state hessen )
(dresden :state sachsen))
If we look at the level of conses and atoms, then we see a binary tree and operations: car, cdr, cons, ... Lisp is there a low-level language: lists are made of simpler building blocks: two-item nodes and atoms.This is a tree:
((a . (10 . nil))
.
((b . (20 . nil))
.
nil))
The dot is an infix syntax element for a cons cell.Which as a nested list is shorter written as
((a 10) (b 20))Lisp de-prioritizes legibility at a glance and concision in order to give maximum priority to simplicity in metaprogramming. I'm not convinced this is a good tradeoff; I would no sooner choose to drive to work in an amphibious tank that got 0.2 miles per gallon to be able to cut across a single river along the way.
Lispers say that I could use specialized tooling to compensate for poor ergonomics, but in many contexts- someone else's computer, a text box or static text on a website, a general-purpose textual diff or search tool, print in a book, a whiteboard- these affordances are not available, and I would still have to contend with the inconvenient reality of Lisp's very real design choices.
I'd need to see a source for this claim to believe that S-exprs are primarily for the benefit of metaprogramming. They have a lot of ancillary benefits in terms of uniformity, simplicity, and scope. Plus, learning paredit gives a lot of power at the keyboard, and you can't build an editing setup without something like S-exprs (or angle bracket equivalents like XML/HTML).
Well, in input- and tooling constrained environments I have preferred Lisp for its syntactic simplicity. I wouldn't call it poor ergonomics.
I love kebab-case-its-so-easy-to-type. May as well be spaces. You can pretty much do everything with 26 lower case characters, the -, space bar, and (). What's that, 30 characters? I have 46 unshifted keys I can use, plus the spacebar. So thats 16 more keys for the more common symbols. Just gotta remap the keyboard a bit.
Recall early Lisp actually used things like (plus ...) and (minus ...).
Not have to chord is a real benefit, to me, when it comes to typing. Lisp just flows that way. For me my-class I find a lot easier than either MyClass, or my_class.
Lisp is optimized for interactive programming user interfaces. There is a lot of tooling around that.
It's pretty common today for people to voluntarily put SQL keywords in all caps.
> Lisp de-prioritizes [...] concision
Really?
Yeah, Lisp can be verbose due to the lack of built-in syntax for things other than lists.
Take the Common Lisp syntax/library functions (there's not really built-in syntax) for hash tables:
https://cl-cookbook.sourceforge.net/hashes.html
Compare it with Python dictionary syntax:
https://developers.google.com/edu/python/dict-files
Almost everything that's built-in to other languages has to be given a name that makes sense in Lisp. It makes it more readable in some ways, but also more verbose.
For a short hash-table notation, I now use Serapeum's dict:
(dict :a 1 :b 2)
and that's it, you now have a hash-table (with a representation than can also be read back in by the lisp reader).A-list:
(("the" . 50) ("be" . 25))
Dictionary literal: {"the": 50, "be": 25 }The Clojure approach of recognizing associative structures as a fundamental building-block and giving them specialized syntax:
{:the 50 :be 25}
Is a clear-cut improvement.I do covet the way clojure's associations are callable as a lookup method, though.
The strength of lisps syntax basically boil down to a few things:
1. Scope is visually clearer than in non-lisps, you dont need to hold the parser in your head while reading code.
2. Code as lists allows consistent and simple macros.
3. No need to reinvent editor tooling.
Maybe all programming languages have their sweet points that learners find "magic" and then generate blog articles.
Except it doesn't - because after the final parenthesis of the Lisp snippet, the let-binding is finished, i.e., the introduced variables are no longer in scope. That is not the case after the end of the last line in the Rust example.
The whole point of parentheses is to mark where something starts and where it ends. Here, it is where the scope of the let-defined variables starts and ends. Without some sort of "parenthesizing", there might be a start, but it's much harder to mark where something ends.
Not to mention that you can always introduce a new lexical scope with `{ ... }` in Rust code.
The greatest strengths for me are the scoping and how lisp enables interactive development. For one, I don't even see parens anymore when writing code. They've become automatic where I'm able to parse structure at least better than I have in the past. With a REPL, (I specifically use Clojure), development has become fun again. A typical debugging process is attaching to my actual program, and calling functions seeing what happens.
I love the flow of defining functions in my code, `def`-ing some test data, and calling those functions. I'll use `cider-inspect` to see if the data structure looks correct and iterate.
Everyone should at least try a lisp. Iterative, interactive development is so fun and powerful.
If you're not a narc,
I've got the goods.
Try a little bit of this julia!
end
Ok, ok I know your parents warned you about indexing from 1 but they also thought that global variables should be replaced with singleton factories, so what do they know. (define (fac x)
(if (<= x 0) 1
(* x
fac (- x 1))))
Still one pair missing.Is it true that in normally formatted LISP code, every line should start with an opening parenthesis?
Btw, I prefer Haskell's syntax of
fac x = if x <= 0 then 1 else x * fac (x-1) (foo bar baz)
(foo
bar baz)
(foo
bar
baz)
All the same thing to lisp DO 10 I=1.10
DO10I = 1.10But there's a mandatory six columns of whitespace at the start of every line, so not quite! But the fact that PRO GRAM and such constructs are legal is... special.
Writing Haskell feels quite Lispy in that the basic syntax is quite generic, but you can nest parentheses all day long without changing the meaning, or even use some generic operators ($, .) to write out a structure in a clearer way. I'd love to see an attempt at a Lisp syntax that felt similarly. (And yes, I know there have been 2^37 attempts at different Lisp syntaxes, but I still haven't found one in line with this idea)
I think implementing operator precedence in ANY programming language is a mistake. `1 + 1 * 2` should be a syntax error. In C, in Python, in Java.
A total ordering has a definite answer to the question "is x greater than, equal to, or less than y?" for all x and y. A partial ordering will answer "I don't know" for some x and y.
It's quite possible to implement operator precedence using a partial ordering. When the parser has to resolve precedence between two operators and their relationship is not defined, throw an "ambiguous precedence" error.
You can implement the partial ordering by putting all the operators into a DAG of sets of operators. If two operators are in the same set, they have equal precedence. If there is a path through the DAG from one operator to the other, that defines the precedence relation. Else there is no relation.
Say "*" is defined to have a higher precedence than "+", and they both have an undefined precedence relation with "&". Then "1 + 2 * 3" should compile into "1 + (2*3)", but "1 + 2 & 3" should throw an "ambiguous precedence" syntax error.
For the same reason that math has defined precedence (and even omits multiplication signs entirely in many places), programming languages in some domains will choose to do the same.
It’s not a problem if you want to create a language that does not do this, but I think it is a problem if you want to ban others from doing it.
Even multiplication is often distinguished more by the shape of the text than by actual precedence rules, once you leave basic arithmetic - expressions like "1 - 3xy + 2y²" make the order of operations obvious from text layout, which is why they are preferred over "1 - 3 × x × y + 2 × y ^ 2", which no mathematician ever writes.
Similarly, in the following expression:
x
_ + 2
2
I don't think it's precedence that makes it clear you first divide by 2 and then add 2 to the result. y = x / 3 + 2
y = 2 + x / 3
y = x/3 + 2
We all agree those are the same, despite a lack of an any cues in the first two. That suggests to me that there is a defined precedence for division vs addition in math. (So much so that it doesn't even seem like a controversial statement to make.)People are generally bad at remembering arbitrary operator precedence rules. That is why most math notation doesn't rely on precedence rules to be unambiguous, it uses other kinds of textual clues. For example, the / operator is virtually never used in algebra. Instead, division is indicated by fractions, which explicitly group their operands and separate them from other adjacent operations. Exponentiation relies on super scripts to separate its arguments, making it extremely unambiguous at least which operations are part of the exponent and which the total. Roots use the radical symbol which extends over the entire expression you're taking the root of, and again rely on a kind of superscript for indicating which root you are taking. Limits and sums use subscripts to separate the iteration direction from the expression. Integrals use both sub and superscripts.
The textual representation even for exponents is not enough if you don't already know that exponentiation has higher precedence than multiplication.
"1 - 3xy + 2xy²"
Both of us know that only the y term is squared in that expression. How do we know that?
I claim that we know that because we know the precedence rules and that someone never exposed to our system of algebra would not inherently and unambiguously know that it was 2·x·(y²) rather than (2·x·y)² or even 2·(x·y)² merely by visual inspection of the expression.
This (the meaning of the above expression, including the precedence/grouping) is something that has to be taught in Alegbra/Prealgebra courses, and is something that some students struggle with.
1+3
x + 4
In this case, the exponent notation makes it unambiguous that this is (x^(1+3))+4, and not x^(1+3+4).I agree that xy² could mean both (xy)² and x(y²), and that perhaps you need to understand precedence to disbiguate. I would still contend that the notation is defined as "each term has its own exponent, possibly 1 which need not be written" to disambiguate, where terms are separated by parentheses or by other operators.
That is, in my mind, the grammar for usual algebraic notation is something like this:
expression = term (op term)*
expression___________ (expression)?
term = (num | var | fraction | \( expression \) | \/expression)
op = + | - | / | × | ° etc
And the rules are that you first evaluate each term, and then the operators between the terms. You only need to follow precedence rules for those operators between terms, the evaluation of a term is unambiguous.Precedence, under the term "order of operations", predates computers by several centuries. Which is not to apply that it was fixed in stone from the beginning, or perfectly consistent, but mathematics agreed on the modern rules universally in the first couple decades of the 20th century.
This is nonsense. There are no underlying mathematical precedence rules. You're seeing precedence being represented visually, but there is no mathematical difference between 5 3 ADD 2 MUL and 5 3 2 MUL ADD.
MUL and ADD are just binary operators, or if you prefer, functions from U × U to U for some set U.
Your statement is particularly nonsensical as applied to the example you're supposedly responding to. Given some expressions:
5 + 3 5 5
------- ; --- + 3 ; -------
2 2 2 + 3
How do you even form the argument that the difference in interpretation is "simply the graphical interpretation of the underlying precedence rules"? The precedence rules here are obvious, because they don't exist:- Division occurs between the upper operand and the lower operand
- Addition occurs between the left operand and the right operand
It's impossible for these to conflict, so there is no such concept as precedence.
(Precedence also doesn't exist between addition and addition because addition is associative. It is necessary between division and division, and the system is straightforward: lower precedence is indicated by longer lines. Tell me about the underlying mathematical precedence rule that determines that.)
You might want to correct the information here then:
https://en.wikipedia.org/wiki/Order_of_operations#Convention...
> These rules are meaningful only when the usual notation (called infix notation) is used. When functional or Polish notation are used for all operations, the order of operations results from the notation itself.
You could "correct" that to observe that "the order of operations results from the notation itself" is true of all notation, including infix notation, but why? It's not wrong now.
Can you really not tell the difference between "notation" and "math"?
They aren't a fact about mathematics. They're a fact about notation. Operator precedence and order of operations are synonymous.
Math has very well-defined, well-trod conventions that are densely packed into the notation. When people try to write code like this though, it’s hard to read.
All else equal, I’ll take a snippet of code with more characters in it but less work to unpack those characters. Reading linearly is cheap; I get tired reading formulas faster than reading technical prose. Precedence makes the mental parsing nonlinear.
(a + b) / 2π
instead of
(a + b) / (2π)
It seems that math teachers put division above multiplication in order of operations but programming languages almost universally treat division and multiplication as equal and read them left-to-right. I've fixed multiple bugs before that were due to this.
For example the APL syntax for expressions, which I prefer, is that there is no operator precedence.
The operands are associated to operators strictly from the right to the left, regardless which operators or functions are used, which is a much wiser choice than the reverse traditional order of evaluation.
No parentheses are used, except when they are needed to modify the default evaluation order.
a | b || c >> d & e * f
? I think its reasonable to say that a language should not allow thatBut all the precedence rules you're invoking here are useful in isolation, and I see no reason why they shouldn't parse in combination. A linter can use heuristics to flag cases which should be parenthesized, a parser would need a defensible rule for failing a parse. What would that rule be?
If you can do it, someone will do it (in prod, in your product).
Not in expressions like 1 + 3 * 6. Personally, I include the parentheses anyway, as a matter of style, and would write 1 + (3 * 6).
But in expressions like i + 3 > 4 / n? No thank you, (i + 3) > (4 / n) would drive me up a wall. Ymmv.
in c, x == 1 || y == 3 works fine, but unfortunately the bitwise operators have the wrong precedence; x & 2 == 2 is parsed as x & (2 == 2), which is never what you meant. so even the best software designers have made terrible blunders in precedence design
math formulas are a bigger issue; 3*x**2 + x/2 + 1 is a lot better than the fully parenthesized version
However, there are also operations with crystal clear precedence relationships, such as logical conjunction and comparison, and in that case it has to be asked what exactly would the parentheses be there for?
In lisp the answer is: to enable syntactic homogeneity and to enable macros.
But in other languages like Python that don’t have sexpr based syntax, parenthesizing everything would just be pointlessly unreadable.
https://hyperscript.org doesn’t allow mixing different infix operators w/o parentheses
(if (<= lower-bound position upper-bound)
...)
And the following doesn't: if (lower_bound <= position <= upper_bound)
... (+)
And (*)
Are well defined."Unifying Textual and Visual: A Theoretical Account of the Visual Perception of Programming Languages" https://dl.acm.org/doi/abs/10.1145/2661136.2661138 direct pdf link: https://dl.acm.org/doi/pdf/10.1145/2661136.2661138
The paper offers a theoretical account based on the semiology of graphics from Bertin, using Lisp as an example (with very similar examples to Andrey's), but also other languages e.g. Befunge ;-)
As someone whos first programming post BASIC was on a Symbolics machine, coding _conventions_ worked just fine with s-expressions
The Polish notation is probably what most people are reacting to unless you used the reverse version with an HP calculator.
Whitespace isn't significant in C like the author suggests, it is coding convention.
COBOL is a counterexample for whitespace significance not helping with readability.
But for lisp the parentheses was a non-issue with just a few emacs macros.
But that is the same with most modern languages outside of novelties like bf
The trade-off ends up being that metaprogramming becomes harder while syntax gets changed
shrug, do it long enough and the parans disappear:
Q: How can you tell when you've reached Lisp Enlightenment?
A: The parentheses disappear.
~ AnonymousAnother example that could work more than RPN/FORTH (or close to the ; operator in smalltalk) is:
(* 4 (+ 1 2 3))
equals to:
+ 1 2 3 . <EOL>
* 4 <EOL>
The DSL is not LISP based but s-expr to define a DAG like in an spredsheet.
I've learned a lot from LISP and its variants over the years, and I understand the value of LISP syntax for meta-programming, but if any of this was truly compelling we'd have all been doing it for years.
Every language has its own form of Stockholm Syndrome, I guess.
a =
let foo = 10
bar = foo + 32
in foo * bar
b =
1
& (+ 2)
& (* 9)
& (/ 5) 10 [ENTER] foo [STO]
« foo 32 + » bar [STO]
foo bar *Various incredible software which has been produced date back from before the Lisp machine went the way of the dodo. An example of this would be Genera.
Other incredible software has a very specific type of draw which doesn't work for everyone. Examples would be Guix and Emacs.
And yet other software is very niche, leading to it being barely discoverable. As Lisp isn't a hot programming language (like Rust is), these projects don't usually get 'this thing was programmed in Lisp' coverage. Things like NASA's SPIKE, for example (cfr. <https://allegrograph.com/press_room/franzs-allegro-cl-used-f...>).
It's a little hard for me to pick out exactly what makes Allegro CL [0] good at the AI thing. They do say you can also write rules in Prolog, which makes me think the advantage isn't down to Lisp but rather the library code they wrote on top of it.
> Other incredible software has a very specific type of draw which doesn't work for everyone. Examples would be Guix and Emacs.
Yeah, maybe this is one. I almost listed Emacs, but I'm a little torn (also a Vim user so don't trust me here haha). Lisp is inextricable from Emacs, but I'm not familiar enough to say whether or not you would have an appreciably harder time writing an equivalent app in, say, Lua. Maybe! Maybe live reload is core to the experience, etc. Like I said I'm not super familiar.
So yeah, I should clarify that I don't think the software needs to take over the world to meet my criteria here. I think niche software can totally qualify; in fact I think it's more likely to qualify. Basically it's "we wrote a unique, useful program in Lisp that we really couldn't have written in a different language." Does Emacs fit the bill here? Maybe, but I kind of doubt it.
but also the ACL2 theorem prover, used in the industry since the 90s, NASA's PVS provers and SPIKE scheduler used for Hubble and JWT, many companies in Quantum Computing, companies like SISCOG, who plans the transportation systems of european metropolis' underground since the 80s, Ravenpack who's into big-data analysis for financial services (they might be hiring), Keepit (https://www.keepit.com/), Pocket Change (Japan, https://www.pocket-change.jp/en/), the new Feetr in trading (https://feetr.io/, you can search HN), Airbus, Alstom, Planisware (https://planisware.com),
or also the open-source screenshotbot (https://screenshotbot.io), the Kandria game (https://kandria.com/),
and the companies in https://github.com/azzamsa/awesome-lisp-companies and on LispWorks and Allegro's Success Stories.
https://github.com/tamurashingo/reddit1.0/
https://www.ptc.com/en/products/cad/3d-design
https://apps.apple.com/us/app/scorecloud-express/id566535238
Not only that, but the site and Y Combinator as a whole likely wouldn't exist without Lisp.
https://paulgraham.com/avg.html
(Wow! Over 20 years ago!)
Rather than "Lisp gives you superpowers", maybe the story is actually "there were some very effective developers who happened to use Lisp to build their stuff".
See for example https://interlisp.org , an early Lisp system with IDE developed at BBN and later at Xerox PARC. They got the 1992 ACM Software System Award for pioneering work in programming environments.
Grady Booch once said at an Eclipse conference: 'For those of you looking at the future of development environments, I encourage you to go back and review some of the Xerox documentation for InterLisp-D.'"
I think the historic Interlisp-D system counts for "incredible software".
There is a bunch of incredible software written in Lisp, from language-oriented workstations, parametric CAD systems, databases, etc. Many languages have "incredible" software written with it, you'll find those for Lisp, too, over the decades it exists. But you might need to very specialized domains, like computer algebra systems, autonomous robots for inspecting pipelines, chip design verification or quantum computers.
Are there, from time to time, language features that really set a language apart? Yeah I think so, Ada has a bunch of baller features, Erlang's OTP is pretty unique, etc. I think these are the kinds of things you could point to and say, "I wrote a network server that processes random user-generated text as fast as possible and has zero memory bugs because I used Ada's safety and performance features" or "I wrote a sprawling telephony system with insanely high uptime because of OTP". They're like the materials science advances right, we found a new way to make a metal that will never bend and is virtually weightless, enabling us to build a space elevator.
Is Lisp like this? Probably! Its ideas of dynamic typing, garbage collection, no direct memory access, interpretation, etc. have been nearly entirely co-opted by static languages like Java and dynamic languages like Lua. I guess my argument is that now, I don't really see a differentiator outside of macros. Maybe it's unfair, but I think my threshold here is "we used macros to build AI/something nuts". I don't think that's happened.
Then again I'm not embedded in the Lisp community -- I definitely got everyone to list their favorite Lisp projects here haha. Maybe some of these examples qualify? I'm not gonna run them all down. I just think there's something rich here about community, ethos, and technologies that find their ways through different languages for technical and cultural reasons.
There is no such thing as "a" Lisp community, since Lisp is a term for a wide variety of different languages, each having their own community. Common Lisp is a group of implementations (which are sub-communities), Scheme is a group of implementations / sub-standards (which are sub-communities), there are derived languages (-> Clojure)...
> I think languages/platforms are less about the technology or features and more about shared values and community.
Yes.
> I guess my argument is that now, I don't really see a differentiator outside of macros.
The larger topic is "symbolic computation" and Lisp itself is one application of it. Lisp cannot only be used to implement itself, but a range of languages. "Interpreter" in Lisp actually means "interpreting Lisp", where interpreter for Java means "interpreter for JVM byte code", which is something very different. An interpreter written in Lisp for Lisp interprets source code, which is actually data.
Scheme, Clojure, ML and a bunch of more esoteric ones were first implemented in Lisp. Often languages and language extensions are integrated into Lisp. Racket (a variant of Scheme) makes language implementation one of its core features.
> Maybe it's unfair, but I think my threshold here is "we used macros to build AI/something nuts". I don't think that's happened.
There are many many usages of language extensions via macros, which are "nuts". For example the possibly first parametric CAD system called iCAD was based on Lisp + a dynamic object system, whose user-facing forms are macros. The language Lisp was thus extended to describe complex parametric CAD models. I once heard that Boeing had whole models of complex jets (think 747) described and loaded into Lisp memory. Turbines were described in Lisp and design changes could be generated via code&data changes. The cabin configuration of jets were described and computed with design rules in Lisp.
One would think that if they really were that great, there wouldn't have been any need to keep writing such articles after several years, and yet here we are again.
> humans can’t immediately parse structure with a glance
No, humans absolutely can, as long as those structures are: a) delimited; and b) the delimiters used aren't nauseatingly repeating. LISP's parentheses start failing at the b) condition much sooner than semantic whitespace/indentation starts to.
One would think that if the earth was really round, there wouldn't have been any need to keep writing such articles after several years, and yet here we are again.
> humans can’t immediately see curvature with a glance
No, humans absolutely can, as long as those curvatures are: a) intuitive; and b) the curvatures used aren't nauseatingly large. Round Earth start failing at the b) condition much sooner than a flat-earth model.
In short, generations are forced to effectively re-learn basic facts about the world they inhabit because new members are in fact ignorant and must be disabused of their intuitive yet incorrect assumptions.
Are you, in fact, comparing a preference for Algolic syntax over s-exprs, to... flat Earth theory?
Surely not.
The criterion of truth is practice. In practice (of the last sixty years), human programmers have generally fared better with non-LISP syntax, like it or not. Sure, if you love the parentheses, go and use them but don't try to pass you personal preference as the objective truth. It's the similar thing with the literate programming: yes, it works great for Knuth, but it doesn't work well at all for most of the other people who are not Knuth.
And by the way, if you drop parentheses and reverse the order of the words, you basically get a FORTH program which is self-delimited and requires no parentheses. So parentheses hit that middle point between the two extremes of "many special pieces of syntax for structuring the program" and "no special syntax for structuring at all" and take the worst from both of them: they are redundant for the machine and they don't help the human programmer ("humans can’t immediately parse structure with a glance"), so all they do is just increase visual clutter. Thanks no thanks.
In Lisp the "paren-heavy syntax" was for humans. It was even a part of M-exprs. It was the syntax for data in the m-expression syntax.
But the M-exprs wasn't implemented, the code was translated by humans to full s-expressions and entered that way.
Users then found out that it was easier to stay with s-expressions.
That doesn't sound superficial to me.
to me, this indicates that iverson was mistaken: apl users did not gain major cognitive advantages over users of programming languages with more conventional syntax, so language choice has been driven by other factors. the same cannot be said about programming language choice in general; writing things in assembly language clearly takes much longer than writing them in c, python, golang, lua, scheme, or basic. but that turns out to be a result of their different data models, not different syntax
Brackets, square brackets, curly brackets.
Lisp programmers see the exact same thing as non Lisp programmers. This image is funny because Lisp is known for its use of parentheses and no amount of coping will change this.
I doubt there is anything nontrivial but useful to say about them?
Anyone found anything in the article?