Yet another alternative lisp syntax

This post adds yet another alternative syntax for lisps. I propose adding both infix operators and function call syntax, but nothing more. This allows the language to be read easily for non-lispers while still remaining white-space agnostic.

Function call syntax

Let’s use function call syntax. Almost like M-expressions but without the unnecessary semicolons:

(I’ll use racket as example) Lisp:

(define (factorial n)
        (if (<= n 1)
            1
            (* n (factorial (- n 1)))))

Alternative:

define(factorial(n)
        if(<=(n 1)
            1
            *(n factorial(-(n 1)))))

Infix operators

Now make operators infix with the following fixed precedence:

  1. Exponent: “^”
  2. Factor: “*” “/” “%” “.”
  3. Term: “+” “-“ “&&” “||”
  4. Comparison: “==” “!=” “>=” “<=” “>” “<”
  5. Assignment: “=”

Or whichever precedence and symbols you prefer.

  • Also, we need to be able to override the precedence when necessary. For that, we use parentheses which will luckily never be ambiguous since they will always surround some infix operator what will not parse otherwise.
  • All operators are left-associative.
  • Escape operators with backticks, i.e. `+`(x y) is the same as x + y.

Alternative with infix:

define(factorial(n)
        if(n <= 1
            1
            n * factorial(n - 1)))

Assignment

Also create a macro to let equals (=) be “define”:

factorial(n) =
        if(n <= 1
            1
            n * factorial(n - 1))

The parentheses just melt off. No need for white-space or indentation-sensitive syntax.

Uniform function call syntax

Let’s do just one more thing.

Okay, two: First, use square brackets for lists as usual. E.g. [+ 1 2 3 f(x)] will translate to (+ 1 2 3 (f x)) — no implicit list or quote, and no semicolons or commas necessary. It provides an escape hatch of sorts. We could have swapped parentheses and square brackets but I like the way f(x) for function calls look more than f[x].

Second, use the dot operator (“.”) in a macro to change the order of operations, almost like uniform function call syntax (well, without juxtaposition):

(Since lisp already uses “.” for cons, let’s rewrite “.” to “dot” internally)

define-syntax(dot
    syntax-rules([]
        [arg.f(args `...`) f(arg args `...`)]
        [arg.f f(arg)]
    )
)

(Note that the macro is written in our syntax: arg.f is rewritten to f(arg) — Remember that under the hood we are actually passing ((dot arg f) (f arg)) to racket.)

Now, let’s do an example that illustrates the advantage (quicksort).

Before:

(define (qsrt iarr lt)
  (cond
    [(< 1 (length iarr))
     (let (
           [pivot (first iarr)]
           [gt (lambda (l r) (not (or (lt l r) (equal? l r))))])
       (append
        (qsrt (filter (lambda (x) (lt x pivot)) iarr) lt)
        (filter (lambda (x) (equal? x pivot)) iarr)
        (qsrt (filter (lambda (x) (gt x pivot)) iarr) lt)))]
    [else iarr]))

After:

qsrt(iarr lt) = if(1 < iarr.length() 
    proc(
        pivot = iarr.first()
        `<`(l r) = lt(l r)
        `>`(l r) = not((l < r) || (l == r))
        append(
            iarr.filterl(lambda([x] x < pivot)).qsrt(lt)
            iarr.filterl(lambda([x] x == pivot))
            iarr.filterl(lambda([x] x > pivot)).qsrt(lt)
        )
    )
    iarr
)

(I also added “proc” as a shorthand for “let” without any bindings, which is probably a bad idea but looks cool. Also, “&&” and “||” and the comparisons were defined as necessary, and filterl is just filter where the list to be filtered is the first argument so we can use dot on it.)

Conclusion

  • We can have less parentheses while still not needing sweet-expressions, i-expressions, semicolons, or comma-separated list elements.

  • The user can indent exactly as they want. Or they can automatically format. Indentation does nothing and we don’t need semicolons to end expressions.

  • We can have uniform function call-like syntax with an operator that works like any of the other infix operators. Note that spaces could be added before or after the “.” just like with “+” or “*” and it would work the same.

  • We don’t really need special syntax for list, array, or hashmap referencing. Just use dot e.g. d.hash-ref(3).

  • Cond and let still looks ugly: e.g. cond([x > 0 "yes"] [else "no"]) or let([[x 1]] display(x)). I couldn’t get pairs [else "no"] to be anything else than list pairs because the cond macro is always evaluated first. Let me know if there is a way to replace pairs with something like => so that we can write cond(x > 0 => "yes" else => "no"). On the other hand, using the square brackets isn’t too bad, and it is nice and close to the original so not too many surprises when translating to and from lisp.

  • We need a way to escape operators, something like backticks `+`. Although backticks and commas are already used as shorthand for quasiquote and unquote in many lisps. Many other shorthands are commonly used in most practical lisps that our language will have to have escape hatches for them or exceptions built in. E.g. “’” for quote. Real lisp does not have an entirely uniform syntax.

  • I don’t think we really need unary operators. There isn’t much difference between not(expression) and ~expression. To negate something, define something like neg(x). Negative literals like -42 can still work though. Maybe there is a way to make unary ops work nicely but I couldn’t find it.

  • We can go overboard and define “:” and “,” operators to do all sorts of amazing things, but it is probably unnecessary. Parentheses makes sense to show the end of an expression or context — no reason to let the user learn something else that will only replace the closing parenthesis with another exotic operator. B.t.w. this post is largely inspired by liso, which I think is awesome! I’m exploring a slightly simpler space of operator-based syntaxes here.

  • It should be possible to do auto-formatting and automatically translate from lisp to this and back (although where exactly to use dot-notation probably depends on personal taste).

  • I don’t know anything about lisp so take things here with a pinch of salt.

This language is so simple it must surely have been invented before. If you know what it is called, please let me know.

Otherwise, for now, let’s call it mi-expressions for M-expression-like function calls with Infix operators.

shaunlebron has a nice history of alternative lisp syntaxes.

Written on December 15, 2024