What’s this about?
Spaced repetition (SR) is a type of learning technique which is predicated on a typical, observed memory degradation rate and schedules one’s reviewing intervals accordingly, so as to maximize the knowledge acquisition while minimizing the efforts.
A schematic memory retention curve may look like this:
At the designed reviewing time points (vertical lines in Figure), one carries out a review of the material to be memorized, and sometimes also gives a performance rating. The next review will be scheduled further into the future. Some systems also adjust the intervals dynamically depending on how well the recall performance was, saving efforts for those more difficult learning tasks.
This post shares a custom spaced repetition implementation inside Emacs’s org-mode.
Motivation
There are a number of softwares built upon the idea of spaced repetition. E.g. Anki seems to be a rather popular choice. Inside Emacs, there is a org-drill third party package that allows one to create flashcards from org-mode notes, and offers a few different algorithms to compute the "optimal" review schedule, incorporating your recall performance feedback.
One big issue I found about these implementations is: they all seem to be built around second language learning, in particular, vocabulary acquisition. It doesn’t have to be restricted to vocabulary, of course. But the process of composing all those flashcards from individual pieces of information/facts can be a rather daunting task. Imagine building a flashcard set about, for instance, classes in the C++ language, or the quasi-geostrophic theory in fluid dynamics. There will be a whole array of information, often so tightly inter-linked, that each in its own isolation doesn’t make too much sense.
It is perhaps more appropriate to treat such contents as a whole entity, and ditch the flashcard idea, but retrain the spaced repetition mechanics.
Besides, it would be nice to have the SR tool integrated with Emacs, my everyday note-taking application.
I have been looking for such a system for some time, until I came across a post in org-roam’s forum, where Andrea (due credits to her) shared her own Elisp implementation that integrates a simple spaced repetition workflow flawlessly with org-mode.
I had a quick look at the code, and immediately decided to gave it a try. We had some discussions on some aspects of it, and I made some small changes to the Elisp code to better suit my needs. I’ve been using it for about one and half months. And I’d like to share my experiences in this post.
How does it work?
This SR utility is built around org-agenda, an integral part of org-mode. (Very powerful and useful, highly recommended.)
Example. A TODO item in org-mode may look like this:
Suppose I’ve finished my 1st-time learning about this particular
topic, and I’d like to include this into my review schedule. I’d run
on this heading the custom Elisp function my-set-spaced-repetition
(shown later), which is bound to the hot-key C-c r
(detailed later). This function does
the following:
- append the
:SR:
tag onto the heading, if it is not already there, - increment the repetition counter by 1, starting from 0. This
counter is shown as another tag, following
:SR:
. E.g.repetition0
. - set the review schedule to the next date by picking an interval from a
prescribed list, in this case it would be the following day
(
++1d
, shown later), since it is the 1st review.
The resultant heading looks like this:
I would then review the relevant material, whether it
is included under the heading itself, or stored somewhere else, on the
scheduled date (2022-05-04
).
After that, I would flag this task as DONE, by running on the heading
the org-todo
command (bound to hot-key C-c C-t
by default in
org-mode). This triggers the my/space-repeat-if-tag-spaced
function
(shown later) that I modified from Andrea’s code, and it does the
following things:
- increment the repetition counter by 1, in this case, to
repetition1
. - schedule the next review date by picking the interval
corresponding to this incremented level of repetition. In this case, 2 days
later (
++2d
). - modify the tags of the heading to reflect these changes.
- revert the TODO statue to its previous value, so
DONE
->TODO
.
The resultant heading now looks like this:
The similar process then repeats, according to this repetition interval table:
repetition time | interval (days or months) |
---|---|
0 | 1 d |
1 | 2 d |
2 | 7 d |
3 | 15 d |
4 | 30 d |
5 | 60 d |
6 | 4 m |
Because these headings are decorated as TODO items, they will be
included in the agenda-view. And, because they all have the dedicated
:SR:
tag, I could filter out such review tasks from all other TODOs,
using the built-in Match a TAGS/PROP/TODO query
function of
agenda-view
. The filtered list may look like this:
The Elisp code
Below is the my/space-repeat-if-tag-spaced
function, the core part
of this utility:
(defun my/space-repeat-if-tag-spaced (e) "Resets the header on the TODO states and increases the date according to a suggested spaced repetition interval." (let* ((spaced-rep-map '((0 . "++1d") (1 . "++2d") (2 . "++7d") (3 . "++15d") (4 . "++30d") (5 . "++60d") (6 . "++4m"))) (spaced-key "SR") (tags (org-get-tags nil t)) (spaced-todo-p (member spaced-key tags)) (repetition-n (car (cdr spaced-todo-p))) (n+1 (if repetition-n (+ 1 (string-to-number (substring repetition-n (- (length repetition-n) 1) (length repetition-n)))) 0)) (spaced-repetition-p (alist-get n+1 spaced-rep-map)) (new-repetition-tag (concat "repetition" (number-to-string n+1))) (new-tags (reverse (if repetition-n (seq-reduce (lambda (a x) (if (string-equal x repetition-n) (cons new-repetition-tag a) (cons x a))) tags '()) (seq-reduce (lambda (a x) (if (string-equal x spaced-key) (cons new-repetition-tag (cons x a)) (cons x a))) tags '()))))) (if (and spaced-todo-p spaced-repetition-p) (progn ;; avoid infinitive looping (remove-hook 'org-trigger-hook 'my/space-repeat-if-tag-spaced) ;; reset to previous state (org-call-with-arg 'org-todo 'previousset) ;; schedule to next spaced repetition (org-schedule nil ".") (org-schedule nil (alist-get n+1 spaced-rep-map)) ;; rewrite local tags (org-set-tags new-tags) (add-hook 'org-trigger-hook 'my/space-repeat-if-tag-spaced)) ))) (add-hook 'org-trigger-hook 'my/space-repeat-if-tag-spaced)
The repetition interval table is stored in this list:
((0 . "++1d")
(1 . "++2d")
(2 . "++7d")
(3 . "++15d")
(4 . "++30d")
(5 . "++60d")
(6 . "++4m"))
You can adjust the intervals to your liking.
Below is the my-set-spaced-repetition
function I created, to
facilitate converting a normal org-mode heading into an SR task. The
last line defines the hot-key binding (bound to C-c r
).
(defun my-set-spaced-repetition () "When called on a heading: 1) append the :SR: tag if not already there, 2) set a schedule to the next day (++1d)" (interactive) (if (org-current-level) (progn (let* ((tags (org-get-tags nil t)) (sr-tag "SR")) (if (member sr-tag tags) (message "Already has SR tag.") (progn (add-to-list 'tags sr-tag t) ; append SR to tag list (add-to-list 'tags "repetition0" t) (org-set-tags tags) (message "Added SR tag to heading.") )) (org-schedule nil "++1d") )))) (global-set-key (kbd "C-c r") 'my-set-spaced-repetition)
Some remarks
I’ve been using this rudimentary spaced repetition tool on a semi-daily basis for about one and a half months, and I’m loving it.
I mostly use it to keep track of my learning progress, covering topics like C++ language tutorials, computing algorithms, or books I am reading. These are all rather different use cases from the conventional vocabulary flashcard type of spaced repetition in that they are all rather "chunky" in size.
Sometimes, I don’t actually put any contents under the SR headings. They therefore serve only as a reminder that I should review these materials on the "correct" date.
This saves me a lot of time from creating a big pile of flashcards, a task you typically have to do for a vocabulary acquisition SR system, but is less obvious how to properly carry out (or whether it is even meaningful), for complicated contents like the formulation of a Kalman filter.
One down-side of my adopted approach is that the tasks are often too big, and it is very difficult to accurately assess the recall performance. This is simply because when a heading covers multiple sub-sections, some parts of it may be more easily memorized than others, but they are all reviewed according to the same schedule.
Another, more direct drawback of big headings is that they take a longer time to review. Some of my headings require more than half an hour to go through, even when I can already recall most of the contents rather well. And things only get worse when multiple review threads collide onto the same date. Sometimes I would find myself sitting in front of my computer late at night, with multiple tasks to review but simply not enough of time before the clock turns mid-night.
To counter this issue, I made a small change to Andrea’s version of
the my/space-repeat-if-tag-spaced
function:
I changed the heading tag querying
function call from (org-get-tags)
to (org-get-tags nil t)
. The
last t
flag specifies that the function does not include inherited
tags (in org-mode, heading tags can be inherited from upper-level
headings, or from a global list applicable to the entire org file).
This allows me to turn sub-headings into their SR tasks, possibly having a different thread of review schedule, thus giving me more granular control over the reviewing frequency.
But this small technical change won’t magically solve the entire problem for me. I will have to be more specific about what I’d like to memorize, rather than simply putting book chapter numbers as a SR task. However, I’ve noticed that once I have gone through a couple of repetitions, the important contents in those book chapters would start to emerge themselves. I could take those parts out and turn them into SR tasks. This is both a process of knowledge distillation and rephrasing, and fosters more accurate recall monitoring.