The Editor Wars are long over. TextMate-style editors (Sublime Text, Atom, VSCode) won. Language-specific editors like Jupyter, Android Studio, and such significant use, and somehow even Notepad++ found its niche. Notably irrelevant are both main actors of the "Editor War" - Emacs and Vi. Emacs even more so, there are somehow still enough Vi diehards to keep Vi-style editors (Vim and NeoVim these days) alive. Emacs-style editors lack even that kind of following.
Anyway, what interests me here is not the editors - I've been early adopter of TextMate and never looked back - but the languages they used for their extensions.
Emacs Lisp was much more powerful, and whole programs that ran from within Emacs were written in it. Back then that was seen as strange, but now every editor works like that, so at least in this sense Emacs won. Emacs Lisp was basically a major Lisp dialect, and I guess it still is, as none of the Lisps are terribly popular. It seems that nobody liked it, and back when Emacs was relevant there was constant talk about switching to Common Lisp, Scheme, or anything else. Now it doesn't matter anymore, the whole ecosystem died.
Meanwhile Vimscript was never anywhere as big as Emacs Lisp, and what remains of the the Vim world (with NeoVim) switched to Lua anyway, only keeping Vimscript for backwards compatibility.
#! we can execute Emacs Lisp scripts from terminal, without ever seeing the editor:
#!/usr/bin/env emacs -Q --script (princ "Hello, World!\n")
$ ./hello.el Hello, World!
It's a Lisp of course, so parentheses everywhere.
princ is a human-friendly
#!/usr/bin/env emacs -Q --script ; FizzBuzz in Emacs Lisp (defun divisible (n m) (= 0 (% n m))) (defun fizzbuzz (n) (cond ((divisible n 15) "FizzBuzz") ((divisible n 5) "Buzz") ((divisible n 3) "Fizz") (t (number-to-string n)))) (dotimes (i 100) (princ (fizzbuzz (+ i 1))) (princ "\n"))
The code isn't too bad, but we already run into some minor issues:
(defun ...)defines a function
(dotimes (i n))only does iteration from
n-1, there's no builtin
(princ)only takes one argument, doesn't print newline, and there's no
printlnequivalent function that would just work - I'm actually baffled why they won't let
princtake multiple arguments, it's such an obvious thing to do, and most languages support it just fine
(cond ...)is like
false, also empty list
OK, let's extend Emacs Lisp to be nicer. We'll add
(dorange (i a b) ...) and many-arguments
#!/usr/bin/env emacs -Q --script (defun prints (&rest args) (if (consp args) (progn (princ (car args)) (apply 'prints (cdr args))))) (defun fib (n) (if (<= n 2) 1 (+ (fib (- n 1)) (fib (- n 2))))) (defmacro dorange (i a b &rest body) `(let ((,i ,a)) (while (<= ,i ,b) ,@body (setq ,i (+ ,i 1))))) (dorange n 1 30 (prints "fib(" n ")=" (fib n) "\n"))
$ ./fib.el fib(1)=1 fib(2)=1 fib(3)=2 fib(4)=3 fib(5)=5 fib(6)=8 fib(7)=13 fib(8)=21 fib(9)=34 fib(10)=55 fib(11)=89 fib(12)=144 fib(13)=233 fib(14)=377 fib(15)=610 fib(16)=987 fib(17)=1597 fib(18)=2584 fib(19)=4181 fib(20)=6765 fib(21)=10946 fib(22)=17711 fib(23)=28657 fib(24)=46368 fib(25)=75025 fib(26)=121393 fib(27)=196418 fib(28)=317811 fib(29)=514229 fib(30)=832040
Step by step:
- the naming is really awful,
setqetc. I know these are traditional Lisp names, they're all ass.
conspmeans "is nonempty list"
carmeans "first element of the list"
cdrmeans "rest of the list"
setqmeans "set variable"
&restmeans remaining arguments of a function
- why do we need to do this silliness like
(apply 'prints (cdr args))instead of
(prints &rest (cdr args))or
(prints . (cdr args))?
OK, let's try some super basic functional programming.
#!/usr/bin/env emacs -Q --script (setq list '(1 2 3 4 5)) (setq add2 (lambda (n) (+ n 2))) (print (mapcar add2 list)) (defun addn (n) (lambda (m) (+ n m))) (setq add3 (addn 3)) (print (mapcar add3 list))
add2 as a lambda that adds
2 to its argument. Then we create
add3 that adds
3. Surely that would work right?
$ ./functional.el (3 4 5 6 7) Symbol’s value as variable is void: n
add2 worked, but
add3 didn't, wat? Well here we run into one of the major issues with Emacs Lisp - it does not use lexical scoping. For some insane reason EmacsLisp uses dynamic scoping for everything. This pretty much kills any idea of using functional programming.
Weirdly at some point after everyone stopped using Emacs, Emacs Lisp added optional "don't be broken" mode, where you can request lexical scoping:
#!/usr/bin/env emacs -Q --script ;; -*- lexical-binding: t -*- (setq list '(1 2 3 4 5)) (setq add2 (lambda (n) (+ n 2))) (print (mapcar add2 list)) (defun addn (n) (lambda (m) (+ n m))) (setq add3 (addn 3)) (print (mapcar add3 list))
$ ./functional2.el (3 4 5 6 7) (4 5 6 7 8)
Also what's up with those extra newlines with
princ don't print any newlines, while
Step by step:
(setq lest '(1 2 3 4 5))- we need that quotation mark to distinguish list from a function call, without it Emacs Lisp would try to call function named
(lambda (n) ...)is anonymous function taking argument
(mapcar f list)is
map, another case of awful naming
At least Unicode works. It would be an embarrassment if editor-specific language didn't support Unicode.
#!/usr/bin/env emacs -Q --script (defun prints (&rest args) (if (consp args) (progn (princ (car args)) (princ "\n") (apply 'prints (cdr args))))) (prints (length "Hello") (length "Żółw") (length "💰") (downcase "Żółw") (upcase "Żółw"))
$ ./unicode.el 5 4 1 żółw ŻÓŁW
All right, let's do something slightly more complicated - a Wordle game.
#!/usr/bin/env emacs -Q --script (defun read-file (path) (with-temp-buffer (insert-file-contents path) (buffer-string))) (defun read-lines (path) (split-string (read-file path) "\n" t)) (defun random-element (list) (nth (random (length list)) list)) (defun report-wordle-blocks (guess word) (dotimes (i 5) (let ((gi (substring guess i (+ i 1))) (wi (substring word i (+ i 1)))) (princ (cond ((equal gi wi) "🟩") ((string-match-p (regexp-quote gi) word) "🟨") (t "🟥"))))) (princ "\n")) (defun report-wordle (guess word) (if (/= (length guess) 5) (princ "Please enter a 5 letter word.\n") (report-wordle-blocks guess word))) (setq word-list (read-lines "wordle-answers-alphabetical.txt")) (setq word (random-element word-list)) (setq guess "") (while (not (equal guess word)) (setq guess (read-from-minibuffer "Guess: ")) (report-wordle guess word))
And here's my first try, not amazing:
$ ./wordle.el Guess: raise 🟥🟩🟥🟥🟩 Guess: maybe 🟥🟩🟥🟥🟩 Guess: dance 🟥🟩🟥🟥🟩 Guess: vague 🟥🟩🟥🟨🟩 Guess: haute 🟩🟩🟩🟩🟩
Step by step:
- Emacs Lisp lacks a lot of obvious functions like "read a file", "random element", or "string contains"
- to read a file we need to create "temporary buffer", insert file contents into that buffer, then read the buffer contents
- to readlines, we need to do that, and then
"\n"- that extra
tmeans to ignore empty strings (like the one at the end after final newline) - the whole thing is not quite right, but close enough
random-elementreturns random element from a list
report-wordle-blocksprints colored blocks for Wordle matches
(string-match-p (regexp-quote gi) word)looks like the easiest way to check if a string contains another, which is baffling missing feature for a text editor
- overall so many small things about this code feel just a bit wrong
Obviously not. Emacs was a pioneer of making editors a application platform, and Emacs Lisp was good enough for that role, but both Emacs and Emacs Lisp are really obsolete. Maybe Emacs would have had a fighting chance with a better language, and less GUI-phobia, but history is what it is.
As for the language itself, Emacs Lisp the language is full of weird archaic quirks, misses so many basic features, and modern Lisps do it a bit better. Arguably none of the Lisps is all that great, but if you want to give Lisp a try, Racket and Clojure are much more reasonable.