Hacker News new | ask | show | jobs
by snotrockets 1875 days ago
I don’t think it’s the parents, but the s-expression themselves.

LISPs forces you to maintain the stack for the parse tree in your head, something humans aren’t that great at — s-expressions are the programming language equivalent of center embedding, which is quite alien for human languages (the depth is three at most: compare that to your favorite lisp program)

4 comments

If you've seen 300+ line react components marching off the right of the screen, you'll know that maintaining the parse tree in your head isn't a barrier to popularity.
While this made me laugh a bit, I think there's a meaningful difference between using "tree-like" syntax for all your code (lisp & S-expressions) vs declarative UI descriptions (JSX).

A deeply nested syntax is beneficial for UI work because you can correlate the structure of the code with the interface/document being rendered. S-expressions for HTML/UI in the form of Hiccup-style templates are equally good (if not better) for the same reason.

In JSX however, there is a clear syntactic distinction between behaviour (C-like JavaScript syntax) and interface descriptions (HTML-like element constructors). In Lisps, the uniformity makes it harder to quickly distinguish "behaviour" from "data", which is kind of the point, but comes with a trade-off in readability.

> In Lisps, the uniformity makes it harder to quickly distinguish "behaviour" from "data", which is kind of the point, but comes with a trade-off in readability.

Maybe in theory, but in practice it's easy to tell because in the case of hiccup, the data is data (as vectors) and behavior is behavior (as function calls), those have different syntax in Clojure.

Lisps are excellent at manipulating trees, which is exactly what HTML and the DOM is.

> you can correlate the structure of the code with the interface/document being rendered

Note that you're taking for granted that the document must necessarily be a nested tree structure, which then behooves us to follow in the code.

Which may be right and good (and in any case is foisted on us); but the assumption bears spelling out.

300+ line components are not popular
Not with me they're not, no.
You might be right in the first part, and the "fear" of s-expressions are only expressed as a fear of parenthesis.

But on the second part I think that's the same as for most languages. You end up with nested scopes at the same degree as any c-like language really. But most lisp programmers tend to break out into new functions a bit earlier than let's say JS programmers. I'd argue that normally you'd have to keep track of less depth in a normal Clojure program than you would in a JS program, simply because of how a programmer usually works in those languages.

That's true if you write everything on one line. But how could you fail to notice that most Lisp code is written on multiple lines and indented? The structure is visually laid out, so as not to be maintained in anyone's head.
Line breaks only shorten lines, they do not change the reading direction. Yes, you can start at the bottom and read upwards, but that's unnatural for most.

Compare:

  something.first().second().third()
With:

  (third
    (second
      (first
         (something))))
The end from read must you to understand, and it gets more complicated as your code does. No wonder Clojure's threading macro is so popular, as it would allow you to write it as:

  (-> something first second third)
Fun fact: Lisp was never supposed to be written with S-expressions. They were intermediate representation, for bootstrapping. McCarthy designed M-expression, with function notation, inflix, and sugar'd cond and list; but all that was omitted due to lack of time, and we were left with S-exps.
> Lisp was never supposed to be written with S-expressions. They were intermediate representation, for bootstrapping.

That's not the complete picture.

The early Lisp manual had a definition for Lisp syntax. The Lisp syntax was based on M-expressions for code and S-expressions for data.

Basically what now is

    (append (quote (1 2 3))
            (quote (a b c)))
was

    append[(1,2,3);(A,B,C)]
where the function call uses M-Expression syntax and the data were S-expressions.

Then we have so-called S-Functions, which work with S-expressions. append is such an s-function.

McCarthy then defined a mapping from M-Expressions to S-Expressions, thus that M-Expressions could be represented (not just written, but also in memory) as S-expressions.

In the next step he defined new S-functions called apply and eval, which took M-Expressions as S-Expression data and computed the results of apply or eval operations.

Example use of apply:

   apply[(LAMBDA,(X,Y),(CONS,X,Y));((A,B),(B,C))]
Thus these s-functions could compute with code which was represented at runtime by s-expression data.

Thus such a program would use both code in M-Expression format and compute with code in S-Expression format.

Since these S-functions apply and eval could be themselves translated to s-expressions and get executed, the specific S-functions apply and eval could get executed by a s-expression evaluator.

The code above would then be written:

    (APPLY,(LAMBDA,(X,Y),(CONS,X,Y)),(QUOTE,((A,B),(B,C))))
which in modern way would be written as

    (apply (lambda (x y)
             (cons x y))
           (quote ((a b) (b c))))
Since programs thus were executed / computed as s-expressions, they were input, computed and printed as s-expressions.

Thus the idea of a simple s-expression meta-programming system made the idea of an additional step of m-expression syntax reading/printing less attractive.

You have a strawman example of piping via .member() because those () sometimes have arguments; that's what they are there for. Function application has not gone away; it's just combined with obj.member access. It can easily become an unreadable mess that will need some way of splitting across lines and indenting:

  something.first(other.foo(bar.f(x, y)).memb, z).second(x.y()).third(a, b, c)

This:

  (third
    (second
      (first
         (something))))
is just function notation with the location of the opening parenthesis having been re-examined, and commas removed. Function application notation is found in a myriad languages: sin(cos(pow(x, 2))).

With the above indentation, it's very readable to me; it's very clear that calculation starts with (something) and moves in an outward direction.

  (-> something first second third)
Right, yes, so we have threading macros, and people use them. That's not all that goes left to right. Lisp's ancient progn (including implicit progn) goes left to right, as do the arguments of functions and most macros:

  (defun app ()
    (init)
    (event-loop)
    (cleanup)
    (exit 0))
Sequential binding can break up a nested "point-free" expression, as an alternative to threading:

  (let* ((a (first (something)))
         (b (second fi))
         (c (third se))
     ...)
Of course you could work around s-expressions (and make your ALGOL-formatted language looks like Lisp, a common complaint against my own code by my coworkers), that wasn't my argument.

Most aren't fond of Lisp syntax, regardless of how you dress it up, and thus writing in Lisps doom you to have fewer people to hand over your code to, and I don't think that scarcity is useful. I suspect the reason most don't like s-expression is that it forces the human reader to maintain a "mental stack", an exercise humans are not too good at, as demonstrated by human languages aversion of center embeddings.

You seem to be stuck on this idea of deep function call nesting being an impediment, which is solved by foo.bar().baz() chained syntax using object dot notation.

But most mainstream languages have chained function call notation as a feature.

Furthermore, foo.bar().baz()... chaining is a fairly recently emerging idiom. It has been possible in a number of languages for decades already, but somehow didn't take off. You would hardly see chains of foo.bar().baz().xyzzy() in 1990 vintage C++ code bases, even though 1985 vintage C++ would easily support it.

Anyway, there is a dialect of Lisp which has integrated the dot syntactic sugar into S-expressions, according to this basic idea:

  This is the TXR Lisp interactive listener of TXR 257.
  Quit with :quit or Ctrl-D on an empty line. Ctrl-X ? for cheatsheet.
  Poke a few holes in TXR with a fork before heating in the microwave.
  1> '(quote x) ;; i.e. just like we have a 'x -> (quote x) sugar ...
  'x
  2> '(qref x)
  (qref x)
  3> '(qref x y) ;; we can have a x.y -> (qref x y) sugar
  x.y
  4> '(qref x y z)
  x.y.z
  5> '(qref x y z w)
  x.y.z.w
  6> '(qref x y 3 w) ;; (let's not when it's ambiguous with floating-point)
  (qref x y 3 w)
  7> '(uref x)  ;; ... and a .x.y (uref x y) sugar
  .x
  8> '(uref x y)
  .x.y
  9> '(uref x y z)
  .x.y.z
  10> '(uref x y 3 w)
  (uref x y 3 w)
Embedded compounds are possible, of course:

  11> '(qref (a) (b) c (d) e f (g))
  (a).(b).c.(d).e.f.(g)
I never intended this to be used for chaining! In fact, only in a fairly recent update to the list-builder object, did I fix it so it can do this:

  12> (new list-builder).(add 3).(add 4).(pend '(5 6 7)).(pend 8).(get)
  (3 4 5 6 7 . 8)
Needless to say, the methods have to return the object in order to make this possible. Before the update, the methods didn't have a specified return value.

In the first place, list-builder is an implementation mechanism under the build macro, which expresses it like this:

  13> (build (add 3) (add 4) (pend '(4 5 7)) (pend 8))
  (3 4 5 6 7 . 8)
so there is no reason to use list-builder directly in most code.

This chaining business is a minor benefit (if at all) of the dot notation. The main motivation is to make programming with structures and OOP more ergonomic. It has a big impact for programs that use data structures, because the use of data structures and OOP can pervade the entire program, and is a driver behinds its structure.

Anyway, anyone discussing S-expressions under the assumption that they do not have a dot notation that can be used for function chaining is simply unaware of the research having been done in this area in the TXR Lisp project.

I love that you use 4 indirections before getting to you main, parenthetical, point.