Where parallels cross

Interesting bits of life

The Poor Org-User Spaced Repetition

Too long; didn't read

Use vanilla Org Mode for your space repetitions. I show you how a bit of Elisp (jump to the bottom to try out) can transform your agenda files into an effective learning exercise. You can integrate this with anything, even org-roam!

The problem

Before discovering the (mandatory) Learning how to learn course, I got in touch with the concept of spaced learning. In short a trick to learn effectively: you fix memories by repeating learning sessions over spaced period of times. Now I was already an Emacs user and I was sure Org Mode should have become my knowledge vector. The only thing: external packages required me to encode my knowledge in a certain way to present it to me at given intervals.

It is a problem indeed

I dislike extra effort and always try to make the most of my laziness (attempting to emulate Haskell). And the fact that I had all this headings stored with precious beacons of knowledge pained me: how many doors was that missing knowledge keeping shut for me?

That thought anguished me enough to push me to write some code.

And there is a solution

The idea was: I want this knowledge to pop up in my Org Agenda, then I want to review that knowledge, then I want this rescheduled automatically for later and disappear from my agenda.

This design guided me to the following code:

(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 . "++10d")
                          (3 . "++30d")
                          (4 . "++60d")
                          (5 . "++4m")))
         (spaced-key "spaced")
         (tags (org-get-tags nil t))
         (spaced-todo-p (member spaced-key tags))
         (repetition-n (first (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 'left)
          ;; schedule to next spaced repetition
          (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)

With this any heading like

* Some important knowledge :spaced:
SCHEDULED <someTime>

will appear in my agenda according to the time interval in the spaced-rep-map let over lambda (which is also the title of a lovely Lispy book, by the way). If I tick it done, the program will reschedule it and add a new tag to it :repetition1:, so I can keep track of my progress.

Note that I set the map to the intervals that I found in some reference at the time:

((0 . "++1d")
(1 . "++2d")
(2 . "++10d")
(3 . "++30d")
(4 . "++60d")
(5 . "++4m"))

After 5 repetitions and some months I expect to have learned my knowledge. You can easily set your own period and more (or less) repetitions.

I have used this method for years now and I must say it works for me. At the beginning of the year I coupled this with org-roam notes, and I am loving it.

In particular something I like to do with org-roam is to have special Elisp links in my headings, they look like:

* TODO [[elisp:(org-roam-graph 1 "/org-roam-notes-path/someNote.org")][review someNote]]             :spaced:repetition4:
SCHEDULED: <2020-10-30 Fri>

This way I can review my knowledge through the power of graphviz's dot diagrams looking like:

Sorry, your browser does not support SVG.

See org-roam documentation on how to make the nodes clickable!

Conclusion

I believe this is a precious snippet of my configuration, and I hope you will enjoy as much as I have.

So just run that snippet and create a knowledge task: give a try to space repetition, you may find out how to learn better and more easily!

I am always curious about the reader smartness: how are you going to use this? Just get in touch if you wish to share it and exchange ideas!

Update 2020-09-23

Thanks to Art, I discovered that the above code does not work in a more recent version of Emacs. This is the fixed code that uses the new API:

(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 . "++10d")
                           (3 . "++30d")
                           (4 . "++60d")
                           (5 . "++4m")))
         (spaced-key "spaced")
         (tags (org-get-tags))
         (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 'left)
          ;; schedule to next spaced repetition
          (org-schedule nil (alist-get n+1 spaced-rep-map))
          ;; rewrite local tags
          (org-set-tags-to new-tags)
          (add-hook 'org-trigger-hook 'my/space-repeat-if-tag-spaced))
      )))

(add-hook 'org-trigger-hook 'my/space-repeat-if-tag-spaced)

Comments