Emacs Lisp dojo

A dojo is a meetup format that involves coding on projects or exercises, usually in groups, for the purpose of practice and learning. There are many dojo meetups that run for different audiences. Attendees may be familiar with the London Clojure Dojo.

Our format is:

  1. Everyone suggests ideas for a project.
  2. Break into small groups, each focusing on a project (or on the exercises below).
  3. Work for 90 minutes.
  4. Present some learnings, or maybe head straight to a pub.

Interactive exercises

These are some exercises to help learn and explore Emacs and Elisp. The aim is to fill in the functions labelled __, so that all the Pass: messages show t instead of nil.

They are inspired by the interactive Clojure exercises on 4Clojure.

Alternatively, you may want to explore the open-ended project ideas at the bottom of this page.

Working with this org-mode file

This web page is written as an org-mode file, which means you can get it on github, open it in Emacs, and execute the code snippets by moving to a code block and pressing C-c C-c. You can also open and edit the code block in emacs-lisp mode by pressing C-c '.

Resources to help

If you're new to Elisp, the resources below can act as starting points or references.


Changes and new exercises are very welcome! Please submit PRs on github.

Strings - uppercase

Transform the given string into uppercase:

(defun __ (s))

(let* ((result (__ "helloworld"))
       (pass (string= result "HELLOWORLD")))
  (format "Pass:%S\n%S" pass result))

Penultimate element

Return the second last element in a list. If there is only one item in the list, return nil.

(defun __ (li) nil)

(let* ((result1 (__ '(1 2 3 4 5)))
       (pass1 (equal result1 4))
       (result2 (__ '("a" "b" "c")))
       (pass2 (equal result2 "b"))
       (result3 (__ '([1 2] [3 4])))
       (pass3 (equal result3 [1 2]))
       (result4 (__ '(1)))
       (pass4 (equal result4 nil)))

  (format "Pass:%S\n%S\n\nPass:%S:\n%S\n\nPass:%S\n%S\n\nPass:%S\n%S"
          pass1 result1 pass2 result2 pass3 result3 pass4 result4))

(Ported from 4Clojure #20)


Return a list with the results of fizzbuzz for a range of 1-100. This means:

  • If the number is divisible by 3, the value is "Fizz".
  • If the number is divisible by 5, the value is "Buzz".
  • If the number is divisible by 3 and 5, the value is "FizzBuzz".
(defun __ () nil)

(let* ((expected '(1 2 "Fizz" 4 "Buzz" "Fizz" 7 8 "Fizz" "Buzz" 11 "Fizz" 13 14 "FizzBuzz" 16 17
                     "Fizz" 19 "Buzz" "Fizz" 22 23 "Fizz" "Buzz" 26 "Fizz" 28 29 "FizzBuzz" 31 32
                     "Fizz" 34 "Buzz" "Fizz" 37 38 "Fizz" "Buzz" 41 "Fizz" 43 44 "FizzBuzz" 46 47
                     "Fizz" 49 "Buzz" "Fizz" 52 53 "Fizz" "Buzz" 56 "Fizz" 58 59 "FizzBuzz" 61 62
                     "Fizz" 64 "Buzz" "Fizz" 67 68 "Fizz" "Buzz" 71 "Fizz" 73 74 "FizzBuzz" 76 77
                     "Fizz" 79 "Buzz" "Fizz" 82 83 "Fizz" "Buzz" 86 "Fizz" 88 89 "FizzBuzz" 91 92
                     "Fizz" 94 "Buzz" "Fizz" 97 98 "Fizz" "Buzz" ))
       (result (__))
       (pass (equal result expected)))
  (format "Pass:%S\n%S" pass result))

Flatten an arbitrarily nested list

Pull out all the nested elements into a single list:

(defun __ (a &rest b))

(let* ((result1 (__ '(1 2 3)))
       (pass1 (equal result1 '(1 2 3)))
       (result2 (__ '(1 2 (3 4) (5 (6 (7))))))
       (pass2 (equal result2 '(1 2 3 4 5 6 7)))
       (result3 (__ '("a" "b") '(c nil d) '(1 (2 3 (4 5)))))
       (pass3 (equal result3 '("a" "b" c d 1 2 3 4 5))))
  (format "Pass:%S\n%S\nPass:%S\n%S\nPass:%S\n%S" pass1 result1 pass2 result2 pass3 result3))

Convert a list into a hash table

Given a list of cons cells (eg. '((a . 123) (b . 456))) create a hash table where the car is the key and the cdr is the value.

(defun __ (s))

(let* ((result (__ '((a . 123) (b . 456) (c . "789"))))
       (pass (equal result #s(hash-table size 3 data (a 123 b 456 c "789")))))
  (format "Pass:%S\n%S" pass result))

Count the lines and words in a buffer

Depending on whether the second argument is the symbol 'lines or 'words, you should return a number that represents the appropriate count. Bonus points if you don't use any of the default line/word count functions.

(defun __ (buffer arg) 0)

  (insert "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore
et dolore magna aliqua. Maecenas sed enim ut sem viverra aliquet. In mollis nunc sed id semper risus
in. In hac habitasse platea dictumst quisque sagittis. Facilisis magna etiam tempor orci eu. Tempus
egestas sed sed risus pretium quam vulputate dignissim. Eros donec ac odio tempor orci dapibus
ultrices. Nam libero justo laoreet sit. Quis imperdiet massa tincidunt nunc pulvinar. Quis imperdiet
massa tincidunt nunc pulvinar sapien et. Consequat semper viverra nam libero justo. Volutpat
maecenas volutpat blandit aliquam etiam erat. Viverra tellus in hac habitasse platea. Condimentum
vitae sapien pellentesque habitant. Eleifend mi in nulla posuere sollicitudin aliquam ultrices
sagittis orci. Vitae elementum curabitur vitae nunc sed velit dignissim sodales. Vel risus commodo
viverra maecenas. Tellus molestie nunc non blandit massa enim. Sed ullamcorper morbi tincidunt
ornare massa eget egestas purus viverra. Donec ultrices tincidunt arcu non sodales neque sodales ut.

Eu tincidunt tortor aliquam nulla. Amet justo donec enim diam vulputate. Tristique senectus et netus
et malesuada fames. Tellus elementum sagittis vitae et. Blandit cursus risus at ultrices mi
tempus. Sit amet purus gravida quis blandit turpis. Metus vulputate eu scelerisque felis
imperdiet. Nulla porttitor massa id neque. Dictum fusce ut placerat orci nulla
pellentesque. Pulvinar mattis nunc sed blandit libero volutpat sed. Amet venenatis urna cursus eget
nunc scelerisque viverra mauris in. Morbi quis commodo odio aenean. Pellentesque massa placerat duis
ultricies. Tristique sollicitudin nibh sit amet. Gravida cum sociis natoque penatibus et magnis dis
parturient. Ut ornare lectus sit amet est. Enim nunc faucibus a pellentesque sit amet
porttitor. Nisl suscipit adipiscing bibendum est ultricies integer quis. Risus pretium quam
vulputate dignissim suspendisse in.")

  (let* ((result1 (__ (current-buffer) 'lines))
         (pass1 (= result1 21))
         (result2 (__ (current-buffer) 'words))
         (pass2 (= result2 283)))
    (format "Pass:%S\n%S\nPass:%S\n%S" pass1 result1 pass2 result2)))

Reimplement mapcar

map is a function that applies a given function to each element of a given list. In Emacs there are a few variants of map functions. Write a function that mimics the behaviour of mapcar. It accepts two argument: the function to apply, and the list of args to operate on.

(defun __ (fn args))

(let* ((result1 (__ 'upcase '("apple" "banana" "pear")))
       (pass1 (equal result1 '("APPLE" "BANANA" "PEAR")))
       (result2 (__ (lambda (arg) (= 0 (mod arg 2))) '(0 1 2 3 4 5 6 7 8 9)))
       (pass2 (equal result2 '(t nil t nil t nil t nil t nil))))
  (format "Pass:%S\n%S\nPass:%S\n%S" pass1 result1 pass2 result2))

Handle errors

You can find some information on error handling in the manual, along with a list of standard errors. Write a function that catches some particular errors and returns their value. Hint: you probably want to use (condition-case).

(defun __ (fn))

(let* ((result1 (__ (lambda () (error "Something went wrong: %s" 'mysymbol))))
       (pass1 (equal result1 "Something went wrong: mysymbol"))
       (result2 (__ (lambda () (/ 5 0))))
       (pass2 (equal result2 "Division by zero"))
       (result3 (__ (lambda () (throw 'catchmeplease "myvalue"))))
       (pass3 (equal result3 '(uncaught . "myvalue"))))
  (format "Pass:%S\n%S\nPass:%S\n%S\nPass:%S\n%S" pass1 result1 pass2 result2 pass3 result3))

Throw and catch

catch and throw are used for control flow, but not necessarily errors. Write a function that executes the given fn, catches the throw from our lambda, and returns the expected value:

(defun __ (fn))

(let* ((result (__ (lambda () (throw 'label "message"))))
       (pass (equal result '(label . "message"))))
  (format "Pass:%S\n%S" pass result))

Use a regex to extract numbers from a buffer

The numbers should be extracted into a list. Note: \d is not supported.

(defun __ (buffer))

(let* ((result1 (with-temp-buffer
                  (insert "There are 2 numbers in this buffer. The second one is 123")
                  (__ (current-buffer))))
       (pass1 (equal result1 '(2 123)))
       (result2 (with-temp-buffer
                  (insert "There are no numbers in this buffer")
                  (__ (current-buffer))))
       (pass2 (equal result2 nil)))
  (format "Pass:%S\n%S\nPass:%S\n%S" pass1 result1 pass2 result2))

Make an HTTP request and parse the JSON response

Make an HTTP request to httpbin.org and convert the "args" from the JSON response into an alist.

(defun __ (url) nil)

(let* ((result (__ "https://httpbin.org/get?one=two&three=four"))
       (pass (equal result '((one . "two") (three . "four")))))
  (format "Pass:%S\n%S" pass result))

Read output from a shell command

There are various ways to call external processes in Emacs. Eval the given argument in a shell and return the result.

(defun __ (shellcommand))

(let* ((result (__ "echo 'always eval user input'"))
       (pass (equal result "always eval user input")))
  (format "Pass:%S\n%S" pass result))

Parse data from an org buffer

Given the string below, convert it to an org-mode buffer, parse out the headline and tags, and return a list where each item looks like ("HEADLINE" . '("TAG1" "TAG2")).

(defun __ (s) nil)

(let* ((org-string "* Headline one    :foo:
* Headline two     :foo:bar:
* Headline three   :bar:")
       (result (__ org-string))
       (pass (equal result '(("Headline one" . ("foo"))
                          ("Headline two" . ("foo" "bar"))
                          ("Headline three" . ("bar"))))))
       (format "Pass:%S\n%S" pass result))

Apply a custom font face to TODO words in a buffer

Each "TODO" word in the buffer below should have my-todo-face applied. You might find it easier to open a test buffer where you can visually check the faces.

When you're done, try adjusting the exercise to support and test multiple faces, or extend the face to apply to the rest of the line matching TODO.

The fic-mode codebase may help you: it's a minor-mode that essentially just highlights some user-defined keywords.

(defface emacs-london/my-todo-face '((t (:inherit 'warning))) "")

(defun emacs-london/check-faces (buffer)
  "This returns a list where each item is t if the TODO font matches my-todo-face, else nil"
  (with-current-buffer buffer
      (goto-char (point-min))
      (cl-loop until (not (search-forward-regexp "TODO" nil t))
               collect (equal (face-at-point t) 'emacs-london/my-todo-face)))))

(defun __ (buffer))

  (insert "import math  # TODO: remove unused import

def main(arg):
    helloworld = 'helloworld'  # TODO: inline this?
    return helloworld

# TODO: execute main()?")

  (let* ((result (progn (__ (current-buffer))
                        (emacs-london/check-faces (current-buffer))))
         (pass (equal result '(t t t) )))
    (format "Pass:%S\n%S" pass result)))

Anagram finder

Write a function which finds all the anagrams in a given vector of words. Your function should return a list of lists, where each sub-list is a group of words which are anagrams of each other. Words without any anagrams should not be included in the result.

(defun __ (v) nil)

(let* ((result (__ ["meat" "mat" "team" "mate" "eat"]))
       (pass (equal result '(("meat" "team" "mate"))))
       (result2 (__ ["veer" "lake" "item" "kale" "mite" "ever"]))
       (pass2 (equal result '(("veer" "ever") ("lake" "kale") ("mite item")))))

  (format "Pass:%S\n%S\n\nPass:%S\n%S" pass result pass2 result2))

(Ported from 4Clojure #77).

Analyse a tic tac toe board

A tic-tac-toe board is represented by a two dimensional vector. X is represented by 'x, O is represented by 'o, and empty is represented by 'e. A player wins by placing three Xs or three Os in a horizontal, vertical, or diagonal row. Write a function which analyses a tic-tac-toe board and returns 'x if X has won, 'o if O has won, and nil if neither player has won.

(defun __ (board) nil)

(let* ((result1 (__ [[e e e]
                     [e e e]
                     [e e e]]))
       (pass1 (equal nil result1))
       (result2 (__ [[x e o]
                     [x e e]
                     [x e o]]))
       (pass2 (equal 'x result2))
       (result3 (__ [[e x e]
                     [o o o]
                     [x e x]]))
       (pass3 (equal 'o result3))
       (result4 (__ [[x e o]
                     [x x e]
                     [o x o]]))
       (pass4 (equal nil result4))
       (result5 (__ [[x e o]
                     [o o e]
                     [o e x]]))
       (pass5 (equal 'o result5)))
  (format "Pass:%S\n%S\nPass:%S\n%S\nPass:%S\n%S\nPass:%S\n%S\nPass:%S\n%S\n"
          pass1 result1 pass2 result2 pass3 result3 pass4 result4 pass5 result5))

(Ported from http://www.4clojure.com/problem/73).

Advice: 2 + 2 = 5

The advice feature allows you to decorate other functions. This means you can patch existing code to change its behaviour.

The first time your function is called, it should advise the + function so that (+ 2 2) returns 5. The second time it's called, it should remove the advise, so that subsequent calls to + return 4 again.

Be careful with this. Breaking (+) could have unintended consequences!

(defun __ () nil)

(let* ((result1 (+ 2 2))
       (pass1 (equal result1 4))
       (result2 (progn (__) (+ 2 2)))
       (pass2 (equal result2 5))
       (result3 (progn (__) (+ 2 2)))
       (pass3 (equal result3 4)))
  (format "Pass:%S\n%S\n\nPass:%S:\n%S\n\nPass:%S\n%S"
          pass1 result1 pass2 result2 pass3 result3))

Open-ended project ideas

Some ideas to get the conversation started:

  • Write a syntax highlighter for a language of your choice.
  • Build a fuzzy browser search interface to replace Spotlight/Alfred. You can use Alvaro's excellent post as a starting point.
  • Write a test runner that can run tests and display their output.
  • Write your own modeline.

See the projects page for more ideas.