Saturday, August 5, 2023

Off-sides Penalty

Many years ago I was under the delusion that if Lisp were more “normal looking” it would be adopted more readily. I thought that maybe inferring the block structure from the indentation (the “off-sides rule”) would make Lisp easier to read. It does, sort of. It seems to make smaller functions easier to read, but it seems to make it harder to read large functions — it's too easy to forget how far you are indented if there is a lot of vertical distance.

I was feeling pretty good about this idea until I tried to write a macro. A macro’s implementation function has block structure, but so does the macro’s replacement text. It becomes ambiguous whether the indentation is indicating block boundaries in the macro body or in it’s expansion.

A decent macro needs a templating system. Lisp has backquote (aka quasiquote). But notice that unquoting comes in both a splicing and non-splicing form. A macro that used the off-sides rule would need templating that also had indenting and non-indenting unquoting forms. Trying to figure out the right combination of unquoting would be a nightmare.

The off-sides rule doesn’t work for macros that have non-standard indentation. Consider if you wanted to write a macro similar to unwind-protect or try…finally. Or if you want to have a macro that expands into just the finally clause.

It became clear to me that there were going to be no simple rules. It would be hard to design, hard to understand, and hard to use. Even if you find parenthesis annoying, they are relatively simple to understand and simple to use, even in complicated situations. This isn’t to say that you couldn’t cobble together a macro system that used the off-sides rule, it would just be much more complicated and klunkier than Lisp’s.

6 comments:

Otto said...

What about Tree Notation:
https://treenotation.org/

an_origamian said...

Another solution (related to Tree Notation) is to take inspiration from Forth. Forth requires no parentheses and ignores indentation, but creation of new syntax is still simple. Of course, Forth can do this because it's more dynamic than even lisp, so a type system would be needed for lisp to allow inference of parentheses at compile-time.

;; Operators
declare - (I &rest 1 I)
declare * (I &rest 1 I)
declare / (I &rest 1 I)
declare ^ (I &rest 1 I)
declare ± (I &rest 1 I)
declare sqrt (I)
declare print (I)
;; Variables
declare a L
declare b L
declare c L

;; Quadratic equation
print / ± (- b)
sqrt - ^ b 2
(* 4 a c)
* 2 a

Heresy time. I like C syntax. Let's make curly braces expand to `progn`.

defun double (v) {
* 2 v
}

expands to

(progn
(declare double (I))
(defun double (v)
(progn
(* 2 v))))

The disadvantage of this syntax is that whitespace is not taken into account at all, and I know some people do like Python's syntax.

an_origamian said...

Oh nice… The post ate my code indentation. Well, fortunately if an interpreter is ever written for this the code will still execute since it ignores whitespace. :)

Joe Marshall said...

The way that comments are treated are a good argument against significant whitespace.

an_origamian said...

Test: no indent
    Test: 4-NBSP indent

an_origamian said...

What sort of syntax for the macro call were you trying to use? Macros seem to work fine with most significant whitespace systems I've seen.

Maybe the best solution for off-sides rule is to compromise on macro calls?

defmacro until (condition &rest body)
  `while (not ,condition)
     ,@body  ; Parentheses are inferred, then each form is inserted at the current indentation level.


let ((x 0)
     (y 1))
  until {x = 10}
    print y
    incf x
    setf y {2 * y}

If you don't like the parentheses used in `let`, then Sweet Expressions proposes this syntax (which i think is ugly):

let
  group
    x 0
    y 1
  until {x = 10}
    print y
    incf x
    setf y {2 * y}

`group` would be a macro.