Not all environments have Lisp-aware structural editing. \ Some are only line-oriented. \ How does one go about editing Lisp line-by-line? assets/lisp-lines.png Bright thumbnail with “LISP” written in the center.\ Individual letters are split into “lines” by soothingly soft colorful lines stretching throughout the image.\ Delineating it like Russian lined notebooks they require at schools.\ In the corners, attributions to “Artyom Bologov” and “aartaka.me” are visible. IMAGE_ALT

So I have these two wolves in me: Lisp and ed(1). One problem with uniting these weird parts of me, though: Lisp is not editable in ed! At all. ed is a line-oriented editor, while Lisp is tree-based, with most forms and “statements” spanning multiple lines. ed was made to program Assembly, C, or configs; not Lisp.

So are there any ways to converge on these two: somehow making Lisp line-editable, and somehow making ed(1) Lisp-aware? That’s what I’m tumbling over in this post:

Typical Lisp

Lisp (as written by a professional) is usually condensed into the most effective AST form. No stray brackets on a line of their own. No unnecessary line breaks after control structures. And no way to know where one form ends and another starts. (Unless you use a Lisp-aware editor and/or semantic indentation.)

(defun factorial (n)
  (if (< n 2)
      1
      (* n (factorial (1- n)))))

And even though I have these macros in my and a whole vi(1) setup for Lisp editing, I’m still frightened by the prospect of editing my Lisp code in ed...

"\C-xu": "s/^\\([[:space:]]*\\)(\\(\\(\\((\\(\\((\\([^()]\\{0,1\\}\\)*)\\)\\{0,1\\}[^()]\\{0,1\\}\\)*)\\)\\{0,1\\}[^()]\\{0,1\\}\\)*\\))/\\1\\2/n"
"\C-xw": "s/^\\([[:space:]]*\\)\\(\\(\\((\\(\\((\\(\\((\\([^()]\\{0,1\\}\\)*)\\)\\{0,1\\}[^()]\\{0,1\\}\\)*)\\)\\{0,1\\}[^()]\\{0,1\\}\\)*)\\)\\{0,1\\}[^() ]\\{0,1\\}\\)*\\)/\\1(\\2)/n"
"\C-xs": "s/(\\(\\(\\((\\(\\((\\(\\((\\([^()]\\{0,1\\}\\)*)\\)\\{0,1\\}[^()]\\{0,1\\}\\)*)\\)\\{0,1\\}[^()]\\{0,1\\}\\)*)\\)\\{0,1\\}[^() ]\\{0,1\\}\\)*\\)) \\(\\(\\((\\(\\((\\(\\((\\([^()]\\{0,1\\}\\)*)\\)\\{0,1\\}[^()]\\{0,1\\}\\)*)\\)\\{0,1\\}[^()]\\{0,1\\}\\)*)\\)\\{0,1\\}[^() ]\\{0,1\\}\\)*\\)/(\\1 \\9)/n"

No, even these can’t handle nested parentheses and multiple forms on the same line.

Line-by-Line Lisp (NDLISP)

So what if we somehow made Lisp forms more line-oriented? This is what many university courses do to students:

(defun factorial(n)
    (if (= n 1)
            (setq a 1)
    )
    (if (> n 1)
        (setq a (* n (factorial (- n 1))))
    )
    (format t "~D! is ~D" n a)
)

Repuling. Disgusting. Wile.

But there’s an insight in that! What if we put significant self-contained forms on their own lines and edited them one by one? This is the idea behind JSON Lines (and NDJSON—Newline-Delimited JSON.) So why not have our own NDLISP?

Non-Portable Lines: Threading Macros and LOOP

Clojure changed the world (did it?) with the notion of threading macros. Now one can finally write Lisp with straight top-down causal relations! And subvert deep indentation and all the structure complications coming from that. So threading macros are a good basis for line-based Lisp editing, actually.

(defn fac [n]
  (if (< n 2)
    1
    (-> n
        dec
        factorial
        (* n))))

Except… CL and Scheme are too chaotic compared to Clojure. Brown field vs. green field, y’know. Scheme threading/chain/pipeline SRFI requires explicit threading argument, for example. Common Lisp arrow macros libraries are numerous yet hard to use. CL simply has too many arguments to functions. Including keyword arguments.

But CL has almost infinite potential with its macros. Including the standard LOOP macro. It’s the one with syntax resembling mainstream non-Lisp languages:

(defun factorial (n)
  (loop with res = 1
        for i from n downto 1
        do (setf res (* res i))
        finally (return res)))

LOOP (sorry for all-caps, it’s a convention) can do a lot of things. I did most of Advent of Code exclusively with LOOPs once! Just don’t go too wild with indentation… Oh, wait.

Indentation-based Syntaxes: T-exprs, Wisp, etc.

Line-isolated forms only get one so far. There’s unease and the idea that Lispers actually don’t care about parentheses at all. Lispers care about indentation derived from parens. Thus the plenty indentation-based syntaxes deprecating parentheses and lines:

These remove one hard part of editing Lisp: parentheses and blocks. But such syntaxes also introduce a significant problem: indentation. Indentation is invisible and indiscernible. Is it five spaces or four? A tab, maybe? What width is a tab even? Does anyone use tabs in the modern world? (They do, they do, they do...)

Spaces are hard, in ed too. Most lines have both a leading and contained spaces. Batch operations on lines are risky. Learning which type of spaces is used in the codebase requires constant (unambiguous printing.) And I prefer (printing with line numbers!)

The fact that I wrote a Wisp-aware ed script defeats my point, but let’s ignore it and see where the post gets us.

Whitespace-Insensitive: Lua

So what if we actually forgot about both parentheses and indentation? (To an extent, I mean!) We get Lua:

function fact(n)
  if n == 0
    then return 1 end
  if n < 0
    then return 0 end
  return n * fact(n-1)
end

True, some forms (, , ) need an . But most regular forms, like assignments, function calls, and return statements—don’t! So making a line-based Lisp means designing an unambiguous set of operations. Mapped from special forms. And extensible with macros. (That’s the hardest part, not sure if it’s even doable.) Like Dylan, but stripped down.

Whitespace-Insensitive + Minor Delimiters: Lamber!

This unambiguous basics type of thing is exactly what Lamber does. Establishing an unambiguous set of fundamental forms and a set of shortcuts. And being a reasonable non-homoiconic (means no macros yet) syntactic Lisp otherwise:

def factorial function (n)
  if (= n 0)
   then 1
  else
   * n : factorial : dec n .

At this point, as an ed fan and a biased programming language author—I’m satisfied with the syntax. One problem though... Extrapolating this toy language syntax to Common Lisp

Middle Ground: Reader Macro

Even if there’s a way to bring Lamber’s a-ha! moments to Lisp... It requires a full-blown Lamber parser. And a set of complex transformations from Lamber to Lisp. Not really the best time I can imagine.

So why not use Lisp-native reader macros, settle for some less, and retain the best parts? Say, Wisp’s colon operator coupled with general whitespace-insensitivity of Lamber/Lisp. Behold, hash/slash operator!

(defun factorial (n)
  (if #/ < n 2
      1
      #/ * n #/ factorial #/ 1- n))

;; Same as
(defun factorial (n)
  (if (< n 2)
      1
      (* n (factorial (1- n)))))

While it doesn’t remove all the parentheses (it doesn’t have to!) And doesn’t make the code readable to a standard Lisp user. It makes things much easier to edit in a line-based environment. I’ve reached what I need. Did you?