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:
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)