Tutorial for Curly Infix, Modern-Expressions, and Sweet-expressions: Readable formats for Lisp-like languages

by David A. Wheeler, 2007-01-06 (Revised 2008-01-09)

Many people find Lisp s-expressions hard to read as a programming notation. This paper briefly describes 3 layered alternatives I've developed (curly infix, modern-expressions, and sweet-expressions), and shows how to download and run free-libre/open source software that implements them. These formats are not tied to any particular Lisp system, and can do everything regular s-expressions can do. You don't need to be familiar with Lisp-like languages to understand this tutorial.

What's wrong with Lisp and s-expressions?

Lisp-derived systems normally represent programs as s-expressions, where an operation and its parameters is surrounded by parentheses. The operation to be performed is identified first, and each parameter afterwards is separated by whitespace. So the traditional “2+3” is written as “(+ 2 3)” in a Lisp-derived language.

S-expressions make it unusually easy for programs to read, process, and generate programs. This can make some programs considerably easier to create, as well as much easier to automatically analyze or prove.

But this format can be hard to read. Lisp fails to support infix notation (by default), for one thing. Its notation for making function calls is completely different than most programming languages and math books, making it unnecessarily harder to read. And it requires using and balancing endless parentheses - editors can partly overcome this, but why use a notation that's so bad that such tools are necessary to compensate for it? It'd be better to have a notation that people can easily read in the first place.

So I've developed three notations: curly infix (which provides an infix notation), modern-expressions (which takes curly infix and adds traditional math function notation), and sweet-expressions (which takes modern-expressions and adds indentation as meaningful). Each one adds an additional feature to the previous notation, with sweet-expressions being the most readable. Take a look at some examples, and compare sweet-expressions to the ugly old s-expressions:
(Ugly) S-expression Sweet-expression 0.2
(define (fibfast n)
  (if (< n 2)
    n
    (fibup n 2 1 0)))
define fibfast(n)  ; Typical function notation
  if {n < 2}       ; Indentation, infix {...}
    n              ; Single expr = no new list
    fibup(n 2 1 0) ; Simple function calls
(define (fibup max count n-1 n-2)
  (if (= max count)
    (+ n-1 n-2)
    (fibup max (+ count 1) (+ n-1 n-2) n-1)))
define fibup(max count n-1 n-2)
  if {max = count}
    {n-1 + n-2}
    fibup(max {count + 1} {n-1 + n-2} n-1)
(define (factorial n)
  (if (<= n 1)
    1
    (* n (factorial (- n 1)))))
define factorial(n)
  if {n <= 1}
    1
    {n * factorial{n - 1}} ; f{...} => f({...})

Note that you can use traditional math notation for functions; fibfast(n) maps to (fibfast n). Infix processing is marked with {...}; {n <= 2} maps to (<= n 2). Indentation is significant, unless disabled by (...), [...], or {...}. This example uses variable names with embedded "-" characters; that's not a problem, because the infix operators must be surrounded by whitespace and are only used when {...} requests them.

It's actually quite common to have a function call pass one parameter, where the parameter is calculated using infix notation. Thus, there's a rule to simplify this common case (the prefix {} rule). So factorial{n - 1} maps to factorial({n - 1}) which maps to (factorial (- n 1)).

Credit where credit is due: The Fibonacci number code is loosely based on an example by Hanson Char.

In short, "Sweet-expressions" are a mostly-backward-compatible format of s-expressions that are easier to read. A sweet-expression reader will accept traditional s-expressions, but it also supports various extensions that make it easier to read (especially for those less familar with s-expressions). Sweet-expressions are automatically translated into s-expressions, and are carefully crafted so they lose no power.

If you are already very familiar with Lisp-derived languages, see my web page on readable s-expressions which discusses this further.

Installing and running the demo

I've implemented a demo implementation of curly infix, modern-expressions, and sweet-expressions. I really want people to try this out and give feedback.

To try the demos out, make sure you have GNU Guile (an implementation of the Scheme dialect of Lisp), expect, and the software configuration management tool "Subversion" (whose command line name is "svn"). You should also have a Common Lisp implementation; here I'll presume it is clisp, but it doesn't really matter. You should be running on a POSIX system; if you use Windows, you may need to install Cygwin first. Any modern GNU/Linux system will do nicely.

First, download the latest version of the demo and related files using Subversion:

 svn co https://readable.svn.sourceforge.net/svnroot/readable/trunk readable

This will create a subdirectory called readable, so "cd" into it:

 cd readable

Using Curly Infix

Let's first try out "curly infix". Start up your Common Lisp (I'll presume it's clisp), and load the curly-infix demo:

 clisp
 (load "curly-infix.cl")

Any of these three new notations (curly infix, modern-expressions, and sweet-expressions) can also accept normally-formatted s-expressions. So you can type in at the command line:

 (+ 2 3)
and when you press enter you'll see 5.

But "Curly infix" adds the ability to use {...} around infix operators. So just type this in:

{2 + 3}
and you will see 5. Look! You now have a calculator! It's important to realize this is a simple syntactic convenience, just like 'x is abbreviation for (quote x) - it does not change what goes on underneath. So if you enter:
'{2 + 3}
it will respond with '(+ 2 3) - in other words, on reading it switched the expression back to traditional notation.

There is intentionally no support for precedence between different operators. While precedence is useful in some circumstances, in typical uses for Lisp-derived languages and sweet-expressions, it doesn't particularly help. This is not a problem, just use parentheses when mixing infix operators:

{2 + {3 * 4}}

You can "chain" the same infix operator, e.g., {2 + 3 + 4} is fine, and it will map to (+ 2 3 4). Note that the infix operator must be surrounded by whitespace - otherwise, it would have no way to know where the name of the operation begins or ends.

What happens if we do use multiple infix operators in a single list? Instead of trying to guess the precedence, the reader will simply turn it into a list with "nfx" at front. You can then define a macro named "nfx" to process the list.

So here's the real rule: in curly infix, the {...} contains an "infix list". If the enclosed infix list has (1) an odd number of parameters, (2) at least 3 parameters, and (3) all even parameters are the same symbol, then it is mapped to "(even-parameter odd-parameters)". Otherwise, it is mapped to "(nfx list)" — you'll need to have a macro named "nfx" to use it.

You can type control-D (Windows: control-Z) to end this demo.

Using Modern-expressions

Now let's try out modern-expressions, but this time using guile (a Scheme implementation):

  ./modern-guile

Modern-expressions include curly-infix, but adds special meanings to the grouping symbols (), [], and {} if they immediately follow a symbol or list (instead of being separated by whitespace).

This means that you can use more "traditional" functional notation, e.g., f(1 2) maps to (f 1 2). Just type in the name of a function, an opening "(", its parameters (space-separated), and a closing ")". Make sure that you do not have a space before the (prefixed) function name and the following "(". For example, type this in:

 cos(0)
and get a very reasonable response. Here's another - try this:
substring("Hello" 1 3)
This will produce "el".

You can nest them, just as you'd expect:

substring("Hello" 1 string-length("xyz"))

Using function name prefixes is a nice way of showing negation, e.g., -(x) computes the value of 0 - x. So while curly infix by itself doesn't handle prefix functions, modern-expressions can handle them nicely:

{-(n) / 2}

You can even use function name prefixes with traditional binary operators, such as:

*(5 4)

This works with zero parameters, too; if you have a command called "help" (guile does), and choose not to give it any parameters, just type this (without pressing space before typing it in):

help()

You can use f{x + 1}, too, which maps to (f {x + 1}) which then maps to (f (+ x 1)). This makes it easy to pass a single parameter which happens to be calculated using infix. For example:

 not{#t and #f}

Just like traditional s-expressions, spaces separate parameters, so it's important that there be no space between the function name and the opening "(". Since spaces separate parameters, a space between the function name and the opening "(" would create two parameters instead of a single function call. The same is basically true for traditional s-expressions, too; (a b) and (ab) are not the same thing.

Here's the real rule: in modern-expressions, f(...) maps to (f ...), f{...} maps to (f {...}), and f[...] maps to (bracketaccess f), but only when f is a symbol or list. The "bracketaccess" is so that you can write a macro to access arrays and other mappings. Unprefixed (...) and [...] mean "create a list" (as they do in Scheme R6RS), but they may contain other modern-expressions.

Normally, people and pretty-printers will format Lisp code so that parameters inside a list are separated by whitespace, e.g., (a b c), so it turns out that this change in interpretation doesn't change the meaning of typically-formatted modern Lisp code (and you can pretty-print code to fix it). What's more, typical Lisp-aware text editors can work with modern-expressions as they are, without change... so if you don't want to change the way you work, but have a somewhat more readable notation, modern-expressions can help. But we still have to do all that parentheses-balancing, which hinders readability. So let's fix that too. Type control-D to get out of this demo.

Using Sweet-expressions

Sweet-expressions takes modern-expressions and adds indentation as being syntactically meaningful. Let's try them out:

./sweet-guile

Here, an indented line is a parameter of its parent, later terms on a line are parameters of the first term, and lists of lists are marked with "group". A line with exactly one term, and no child lines, is simply that item; otherwise that line and its child lines are themselves a new list. Lines with only a ;-comment, and nothing else, are completely ignored - even their indentation is irrelevant. Whitespace-only lines at the beginning of a new expression are ignored, but a whitespace-only line (including a zero-length line) ends an expression. So, just type in your expression, and type a blank line (an extra Enter) to indicate that you're done.

Here's a trivial example:

substring "Hello"
  1
  3

What happens if the parameters are not constants, but something to be calculated? No problem, just put them on new lines and give them parameters. If something has parameters, then it must be something to calculate too! Here's another example (be sure to press at least one space before the 'substring'):

substring
  "Hello"
  1
  string-length "xyz"
You can use parentheses, too; inside any grouping characters (...), [...], and {...}, indentation is ignored:
  substring
    "Hello"
    1
    string-length("xyz")
Here are some other valid sweet-expressions:
if {7 < 5}
  {3 + 4}
  {5 * {2 + 3}}
abs{0 - 5}

Here's a more substantial example, one we've seen earlier:

define fibfast(n)  ; Typical function notation
  if {n < 2}       ; Indentation, infix {...}
    n              ; Single expr = no new list
    fibup(n 2 1 0) ; Simple function calls
define fibup(max count n-1 n-2)
  if {max = count}
    {n-1 + n-2}
    fibup(max {count + 1} {n-1 + n-2} n-1)

Sometimes you want to have a parameter that is a list of lists, or where the function to be called is in fact determined by another calculation. This is indicated with the "group" keyword; basically, "group" maps into a null function name, so you can use forms like "let" easily.

sweet-filter that lets you read in many sweet-expressions, and outputs the underlying s-expressions. You can use this to process files (say, in a makefile) so that you can write in sweet-expressions, yet be able to see what will be run by the underlying system.

When you're done with the demo, you can exit:

exit()

More Complex Examples

Basic Expressions

If you're familiar with traditional s-expressions, here are more examples. The left-hand-side are sweet-expressions; the right-hand-side are the transitional s-expression forms, though the sweet-expression reader can read them as well:

factorial(z)         <==> (factorial z)
foo(x y)             <==> (foo x y)
bar(x y z)           <==> (bar x y z)
factorial{x - 1}     <==> (factorial {x - 1}) <==> (factorial (- x 1))
f(g(y) h(y) a)       <==> (f (g y) (h y) a)
f({x + 2} y {z - 3}) <==> (f (+ x 2) y (- z 3))
f{{x + 2} * {y - 3}} <==> (f (* (+ x 2) (- y 3)))

What's the big deal?

If you aren't familiar with Lisp, you may say "what's the big deal"? After all, this looks a lot like traditional languages. Many have commented that it looks like Python, with its use of indenting, and of course nearly all other languages use infix notation.

But that's the point - the results look much more familiar (and thus are more acceptable to non-Lispers), but all of Lisp's more exotic capabilities still work. You can use techniques like quoting (') and quasi-quoting (`) with lifting (,), which enable powerful capabilities. Many people have created "infix" notations with Lisp-like languages before, but they all failed to work with many other Lisp features. I think this approach succeeds instead, where others before have failed.

Closing Remarks

Sweet-expressions can take a few minutes to learn how to use, just like anything else new. But I think they are worth it. If you want more details on the rules, and/or why they are the way they are, see the sweet expressions paper.


For more information, see my website page at http://www.dwheeler.com/readable. For example, I provide a sweet-expression reader in Scheme (under the MIT license), as well as an indenting pretty-printer in Common Lisp. In particular, you can see my lengthy paper about why sweet-expressions do what they do, and some plausible alternatives, and my follow-on paper on sweet-expressions version 0.2. You can also download some other implementation code. I've also set up a SourceForge project where options like sweet-expressions can be discussed; if you're interested, please join!