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:
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
No, even these can’t handle nested parentheses and multiple forms on the same line.
So what if we somehow made Lisp forms more line-oriented?
This is what many university courses do to students:
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?
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.
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:
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.
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
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.
So what if we actually forgot about both parentheses and indentation?
(To an extent, I mean!)
We get Lua:
True, some forms (
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:
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
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!
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?
"\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"
Line-by-Line Lisp (NDLISP)
(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)
)
Non-Portable Lines: Threading Macros and LOOP
(defn fac [n]
(if (< n 2)
1
(-> n
dec
factorial
(* n))))
(defun factorial (n)
(loop with res = 1
for i from n downto 1
do (setf res (* res i))
finally (return res)))
Indentation-based Syntaxes: T-exprs, Wisp, etc.
Sweet-expression,
Whitespace-Insensitive: Lua
function fact(n)
if n == 0
then return 1 end
if n < 0
then return 0 end
return n * fact(n-1)
end
Whitespace-Insensitive + Minor Delimiters: Lamber!
def factorial function (n)
if (= n 0)
then 1
else
* n : factorial : dec n .
Middle Ground: Reader Macro
(defun factorial (n)
(if #/ < n 2
1
#/ * n #/ factorial #/ 1- n))
;; Same as
(defun factorial (n)
(if (< n 2)
1
(* n (factorial (1- n)))))