100 Languages Speedrun: Episode 86: Emacs Lisp
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.
Hello, World!
With proper #!
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 print
. A lot of Emacs Lisp functions have such cryptic names.
FizzBuzz
#!/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 from0
ton-1
, there's no builtina
tob
iteration(princ)
only takes one argument, doesn't print newline, and there's noprintln
equivalent function that would just work - I'm actually baffled why they won't letprinc
take multiple arguments, it's such an obvious thing to do, and most languages support it just fine(cond ...)
is likeif/elsif
chaint
meanstrue
nil
meansfalse
, also empty list
Macros
OK, let's extend Emacs Lisp to be nicer. We'll add (dorange (i a b) ...)
and many-arguments (prints ...)
.
#!/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,
progn
,car
,cdr
,setq
etc. I know these are traditional Lisp names, they're all ass. consp
means "is nonempty list"car
means "first element of the list"cdr
means "rest of the list"setq
means "set variable"&rest
means 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))
?
Functional Programming
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))
We create 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
Well 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.
Optional Don't Be Broken Mode
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 (print ...)
? prin1
and princ
don't print any newlines, while print
prints one before and one after, WTF?
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 named1
(lambda (n) ...)
is anonymous function taking argumentn
(mapcar f list)
ismap
, another case of awful naming
Unicode
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
Wordle
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
split-string
by"\n"
- that extrat
means to ignore empty strings (like the one at the end after final newline) - the whole thing is not quite right, but close enough random-element
returns random element from a listreport-wordle-blocks
prints 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
Should you use Emacs Lisp?
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.
And if you want to code some editor plugins, VSCode is all JavaScript, so you'll have to learn that.
Code
All code examples for the series will be in this repository.