top | item 36195276

(no title)

yogsototh | 2 years ago

Personally I find that LISP syntax remove a layer of complexity by directly exposing the AST to my brain instead of adding a layer of internal parsing.

    1 + x * 2 - 3 % x
is longer to decipher than

    (% (- (+ (* x 2) 1) 3) x)
which is itself harder than

    (-> x (* 2) (+ 1) (- 3) (% x))
But it takes a while to be used to it. And yes, it really helps writing macros, but I wouldn't say this as always be a good thing. Macros are far from being the alpha and omega of programming, as they add an implicit layer of transformation to your code making it easier to write but very often harder to read and reason about.

discuss

order

perrygeo|2 years ago

I started working through Crafting Interpreters, building up a language syntax and grammar from scratch. A lot of work and 75 pages of lex/parse logic and we now have a AST... that we can debug and inspect by looking directly at its sexp representation.

It was the ah-ha moment for me... why not express the source cost directly as that AST? Most languages require lots of ceremony and custom rules just to get here. Sexps are a step ahead (inherently simpler) since they're already parsable as an unambiguous tree structure. It's hard to unsee - reading any non-Lisp language now feels like an additional layer of complexity hiding the real logic.

munificent|2 years ago

Much of the complexity and error reporting that exists in the lexer or parser in a non-Lisp language just gets kicked down the road to a later phase in a Lisp.

Sure, s-exprs are much easier to parse. But the compiler or runtime still needs to report an error when you have an s-expr that is syntactically valid but semantically wrong like:

    (let ())
    (1 + 2)
    (define)
Kicking that down the road is a feature because it lets macros operate at a point in time before that validation has occurred. This means they can accept as input s-exprs that are not semantically valid but will become after macro expansion.

But it can be a bug because it means later phases in the compiler and runtime have to do more sanity checking and program validation is woven throughout the entire system. Also, the definition of what "valid" code is for human readers becomes fuzzier.

packetlost|2 years ago

Those rules help reduce runtime surprises though, to be fair. It's not like they exist for not purpose. It directly represents the language designer making decisions to limit what is a valid representation in that language. Rule #1 of building robust systems is making invalid state unrepresentable, and that's exactly what a lot of languages aim to do.

baq|2 years ago

Note that this approach has been reinvented with great industry success (definitions may differ) at least twice - once in XML and another time with the god-forsaken abomination of YAML, both times without the lisp engine running in the background which actually makes working with ASTs a reasonable proposition. And I’m not what you could call a lisp fan.

sbergot|2 years ago

You left out the more common options:

    (1 + x * 2 - 3) % x
and

    (1 + (x * 2) - 3) % x
both are clearer than the S-expression in my opinion

zelphirkalt|2 years ago

I don't find them to be clearer, with the background of knowing many language, because now I have to worry about precedence and I better double check, to not get it wrong or read it wrong.

Paul-Craft|2 years ago

I agree, and this is why for math expressions that aren't just composition of functions that aren't basic operators, I like to use a macro that lets me type them in as infix. It's the one case where lispy syntax just doesn't work well, IMO.

digdugdirk|2 years ago

As someone who isn't a trained programmer (and has no background or understanding of lisp) that looks like you took something sensible and turned it into gibberish.

Is there a recommended "intro to understanding lisp" resource out there for someone like myself to dive in to?

Capricorn2481|2 years ago

  (-> x (* 2) (+ 1) (- 3) (% x))
The part that is confusing if you don't know Clojure is (->). This a thread macro, and it passes "x" through a list of functions.

So it basically breaks this down into a list of instructions to do to x. You will multiply it by 2, add 1 to it, take 3 from it, then do the modulus by the original value of x (the value before any of these steps).

Clojurists feel like this looks more readable than the alternative, because you have a list of transformations to read left to right, vs this

  (% (- (+ (* x 2) 1) 3) x)
Which is the most unreadable of them all, to me.

teamonkey|2 years ago

  '(
    1. This is a list: ( ). Everything is a list. Data structures are all lists.
    2. A program is a list of function calls
    3. Function calls are lists of instructions and parameters. For (A B C), A is the function name, B and C are parameters.
    4. If you don't want to execute a list as a function, but as data, you 'quote' it using a single quote mark '(A B C)
    5. Data is code, code is data.)

kazinator|2 years ago

It is gibberish. The expression means:

  (1 + (x * 2)) - (3 % x)
So the correct S-exp (let's use mod for modulo rather than %):

  (+ 1
     (* x 2)
     (- (mod 3 x)))

It's a sum of three terms, which are 1, (* x 2) and something negated (- ...), which is (mod 3 x): remainder of 3 modulo x.

The expression (% (- (+ (* x 2) 1) 3) x) corresponds to the parse

  ((x * 2 + 1) - 3) % x
I would simplify that before anything by folding the + 1 - 3:

  (x * 2 - 2) % x
Thus:

  (% (- (* 2 x) 2) x).
Also, in Lisps, numeric constants include the sign. This is different from C and similar languages where -2 is a unary expression which negates 2: two tokens.

So you never need this: (- (+ a b) 3). You'd convert that to (+ a b -3).

Trailing onstant terms in formulas written in Lisp need extra brackets around a - function call.

_dain_|2 years ago

In real Lisp code you'd likely indent it something like this:

    (% 
      (-
        (+ (* x 2)
           1)
        3)
      x)
This makes the structure clearer, although it's still wasteful of space, and you still have to read it "inside-out". The thread macro version would be:

  (-> x
    (* 2)
    (+ 1)
    (- 3)
    (% x))
It's more compact, there's no ambiguity about order-of-operations, and we can read it in order, as a list of instructions:

"take x, times it by 2, add one, subtract 3, take modulus with the original x".

It's pretty much how you'd type it into a calculator.

EDIT: care to explain the downvote?

Ologn|2 years ago

> that looks like you took something sensible and turned it into gibberish

This is the main thing I use Lisp (well, Guile Scheme) for. I used to use bc for little scratch pad calculations, now I usually jump into Scheme and do calculations. I don't recall if I thought it looked like gibberish at first but it's intuitive to me now.

mlajtos|2 years ago

Reject PEMDAS, return to monky:

x * 2 + 1 - 3 % x

https://mlajtos.mu/posts/new-kind-of-paper-2

rileyphone|2 years ago

My preferred version of this:

x.*(2).+(1).-(3).%(x)

Unfortunately our brains are broken by pemdas and need clear delineations to not get confused; this syntax also extends to multiple arguments and is amenable to nesting.

todd8|2 years ago

When I learned APL, the expression evaluation order at first seemed odd (strictly right to left with no operator presence, 5*5+4 evals to 45 not 29). After working with it a couple of hours I came to appreciate its simplicity, kind of like the thread operator in your last example.

Tainnor|2 years ago

Well, the easiest way to write

  1 + x * 2 - 3 % x
would just be "x-2".

But if we're talking more generally, if I have an expression like

  2*x^3 + x^2 - 5*x
a trained eye immediately can read off the coefficients of the polynomial and I'm not sure if that's true of

  (+ (* 2 (^ x 3)) (^ x 2) (- (* 5 x)))

zelphirkalt|2 years ago

For writing a program, the s-expression form might become:

    (+ (* 2 (^ x 3))
       (^ x 2)
       (- (* 5 x)))
Whereas:

    2*x^3 +
    x^2 -
    5*x
Would probably error out in most languages, due to parsing issues and ambiguity. Even worse ambiguity, if you put the signs in front, as then every line could be an expression by itself:

    2*x^3
    + x^2
    - 5*x
Could be 3 expressions or 2 or 1.

drBonkers|2 years ago

> `(-> x (* 2) (+ 1) (- 3) (% x))`

Love the single pipe operator. What language works this way?

Folcon|2 years ago

That's a clojure norm, it may exist in other lisps.

agumonkey|2 years ago

That's how my brain feels. it connects informations (compound terms) to entities directly, it's almost minimized information required to represent something, unlike algol based languages.

mmphosis|2 years ago

I prefer infix over prefix. And don't forget postfix

    x 3 1 2 x * + - %
I definitely prefer white-space over commas and other syntactic obstacle courses.

Tyr42|2 years ago

I mean I would probably punch that in as

    x 2 * 1 + (-) 3 + x %
When I was using my rpn calculator. (-) flips the sign of the number on top of the stack.

Or use flip -. But you can avoid growing the stack long enough to be confusing.

kazinator|2 years ago

Using piping for arithmetic is reminscent of grade school arithmetic dictations.

"Start with 10; add 5; divide by 3; ..."

:)

rpz|2 years ago

If the language has no operator precedence then

  1+x*2-3%x
is just as easy if not easier to decipher compared to both of your other examples imo. The above is equivalent to

  (1+(x*(2-(3%x)))) 
in APL/j/k. You get used to it pretty quickly.