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 from 0 to n-1, there's no builtin a to b iteration
  • (princ) only takes one argument, doesn't print newline, and there's no println equivalent function that would just work - I'm actually baffled why they won't let princ take multiple arguments, it's such an obvious thing to do, and most languages support it just fine
  • (cond ...) is like if/elsif chain
  • t means true
  • nil means false, 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 named 1
  • (lambda (n) ...) is anonymous function taking argument n
  • (mapcar f list) is map, 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 extra t 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 list
  • report-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.

Code for the Emacs Lisp episode is available here.