← Back to context

Comment by rscho

12 days ago

People who complain about parentheses have never tried structural editing. Change my mind!

I complain about parentheses, and yet I'm building a structure editor[1]. Two complaints about parentheses:

- Parentheses obscure the structure of the language. When different syntax is used for different parts of the language, it's easier to visually scan the code. This is simply because punctuation/arrows/etc. visually stand out more than plain identifiers.

- Parenthetical languages have just as much syntax as non-parenthetical languages, it's just a shittier syntax. Try writing (let (x 1) (y 2) (+ x y)) and see what happens: it's a syntax error. Look at the indentation rules in DrRacket: there's let-like, cond-like, etc., because all of those constructs have different syntax. But it all kind of blends together in a sea of parens.

This weakness of paren-heavy languages is also their greatest strength, though. Since the syntax is so generic, it's easy to extend, and these language extensions can be indistinguishable from the base language. That's a big deal!

BTW, what structure editor would you recommend? Which have you tried?

[1] https://github.com/justinpombrio/synless

  • I run emacs, so I messed around with paredit and friends.

    I can understand your pov as a professional coder, doing enough coding that you can really master the syntax of your language of choice. I code occasionally for scientific research, and the less syntax I have to remember, the better. Little syntactic constraint in combination with structural editing really is a killer feature in my context.

    • paredit is what I think of as a half-structural editor. It gives you some structural shortcuts, but the cursor is still on a character instead of on a node, IIRC. My bet (that I've spent a lot of time building an editor for) is that the big gains will come when the cursor is never on text, always on an AST node.

      > the less syntax I have to remember, the better

      Part of my point is that parenthetical languages don't actually have that much less syntax. You have to remember one of these two syntaxes:

          let x = 1;
          let y = 2;
          do_stuff
      
          (let
            ((x 1)
             (y 2))
           do_stuff)
      

      Now you say that there's a lot more possible variation in the first case; it could be:

          let x = 1; # Rust
          var x = 1; # JS
          x=1 # bash
      

      And I point out that there's a lot of possible variation in the second case too:

          (define x 1) (define x 2) do_stuff
          (let (x 1) (y 2) do_stuff)
          (let x 1 (let y 2 do_stuff))
      

      There is less syntax with parens. But it's not zero syntax, and you still need to memorize it.

I tried Racked with the recommended IDE setup (VScode), does that have structural editing?

Here are my notes from 2023-07:

https://docs.racket-lang.org/more/index.html

    * okay, seriously. functional programming is nice, but these fucking parenthesis are ridiculous. the VSCode extension is okay, but doesn't help at all with formatting, etc.
    * "car" and "cons", yeey, but "first" would have been so hard?
    * the whole "define x 'x" is also meh.
    * no return, so sometimes something just takes the last one and returns.
    * there's string->url ... why not string->parse-url .. no, would have been too verbose. MAYBE YOU COULD HAVE SAVED SPACE BY OMITTING THE FUCKING PARENTHESES
    *

/ end notes

ehehe ... well ... I think I will keep trying it again every few years. is there a pythonish version, where indentation matters and no need for wrapping parens?

  •     * there's string->url ... why not string->parse-url .. no, would have been too verbose. MAYBE YOU COULD HAVE SAVED SPACE BY OMITTING THE FUCKING PARENTHESES
    
    

    string->url is consistent with the way they do things in Racket. Note in that same document you linked the use of number->string and string->number, the -> indicates a type conversion. Along with string->url there is also the reverse, url->string, and some other conversion functions. That consistency is actually pretty nice, it means you can guess and check ("I have a string and want a url, will this work?" Oh, great it does!) or guess and search the docs before checking with the REPL or whatever.

    https://docs.racket-lang.org/net/url.html

        * "car" and "cons", yeey, but "first" would have been so hard?
    

    car shows up once, cons not at all, but he does use cdr. first, second, and rest are available, I don't know why he didn't use it in this demonstration. If you want to use first, go for it.

  • > these fucking parenthesis are ridiculous

    They're just different. And once you've come familiar with the language, you miss s-expressions everyday, because they're just that easy to work with, especially with something like paredit. Why? because the grammar is easy to parse and reason about. The whole code is a tree. And evaluation is mostly working from the leaves to the root.

    > "car" and "cons", yeey, but "first" would have been so hard?

    It comes from the nature of the language. "cons" is to construct a pair of values, and "car" to get the first one, while "cdr" returns the second one. But lists are composed of cons cells (check how it works), and in that case you could argue for "head" and "tail" for the function names. But "car" and "cdr" were first and you could alias them easily.

    > no return, so sometimes something just takes the last one and returns

    The computing paradigm is expression evaluations, not sequence of instructions (although sequencing is there). An s-expression is always equivalent to something, and that something is what you're computing. Something like (first '("name" "email")) is the same as "name". Or (if (> x 0) :pos :neg) with x = 5 is the same as (if t :pos :neg) and the same as :pos. [0]

    No return is needed. It's tricky when doing iteration, but whenever you're doing sequencing (they mention that in the docs for CL), the last value is equivalent to the whole thing.

    [0]: https://en.wikipedia.org/wiki/Lambda_calculus#Reduction

    • other ASTs are trees too :)

      > It's tricky when doing iteration ...

      and that's my problem, that in the name of simplicity everything nice is thrown out. and "don't even think about it" and "you are holding it wrong" is the official motto. sure, I'm happy to adapt if I feel I got something in return, ie. memory safety with Rust, powerful type system in Scala, etc.

      all in all, sure, it's Turing-complete, and obviously millions of people already grok it and are productive in Lisps, but to me - and apparently to the vast majority of programmers - it's too foreign.

      2 replies →

  • Your notes indeed suggest that you've not been using structural editing.

    Aside from that, you could have tried to use `first` instead of `car`. It would've worked.

    And yes, there happens to be a pythonic version named `Rhombus`, which is the flagship language of the racket system.

  • > but these fucking parenthesis are ridiculous

    I thoroughly agree. I am deeply into functional programming, but syntax built entirely around endless nested parentheses has never felt like anything but a nightmare to me. Doubly so because even in 'clean' code it's reusing the same syntax with what are for most coders three clearly different logical concerns (defining functions, listing statements to execute in order, and invoking functions).

    • > what are for most coders three clearly different logical concerns

      That's the imperative model which foundations is the Turing machine. Lisp comes from lambda calculus and you're not doing the above really. At it's core there's the value (or the symbol that's represent it), then the abstraction, which defines how to get the value, and the application, which let you know with what to replace unknowns (variables) in your abstraction so you can get the value. And it's recursive.

      A program can be a value (not that useful), an abstraction (kinda like a nice theorem), or an application (the useful choice). Defining a function is creating a named abstraction, which is just a special value. Invoking a function is applying the parameters to that abstraction, which means replacing variables to get a more simplified version with the goal to finally have a value. If you can't get a value, that means the result is still an abstraction and then you still have to do more applications.

      You either have a symbol or atom (which is a value) or you have a list which is an application (except the empty list, which is the same thing as nil). An abstraction is created by special forms like (defun foo (bar) ...) in CL. but the result of the latter is still a symbol. An atom is equivalent to itself, and a list is equivalent to having applied the abstraction represented by the first element to the rest. Anything else is either special forms, macros, or syntactic sugar.

      So unless you're applying imperative thinking to the lambda calculus model, there's no confusion possible.

      1 reply →