haskell-fsrs

A Haskell implementation of FSRS-7, the seventh version of the Free Spaced
Repetition Scheduler — the memory
model behind Anki's scheduler.
FSRS predicts when you are about to forget a flashcard so it can be shown to
you just before that happens. It tracks two numbers per card:
- stability — the memory's half-life, in days;
- difficulty — how hard this particular card is for you, on a 1–10 scale;
and derives retrievability, the probability that you can recall the card
right now.
The package version tracks the algorithm version, the way py-fsrs and
fsrs-rs do: 7.x.y implements FSRS-7.
What is new in FSRS-7
FSRS-7 has 35 parameters, up from FSRS-6's 21. Three things changed:
- The forgetting curve is a mixture of two power laws rather than one, with
the mixing weights themselves depending on stability. That is where six of
the new parameters go, and it means the curve has no closed-form inverse — so
computing an interval is a root-find, not a formula.
- The stability update runs twice, once with a long-term weight block and
once with a short-term one, and the two are blended by a smooth transition
function of the elapsed time. FSRS-6 instead switched between two separate
formulas on a same-day / not-same-day flag.
- Intervals are genuinely continuous. Every earlier version was designed
around whole-day intervals; FSRS-7 is the first that gives realistic
predictions for same-day reviews. Ten minutes is
10 / 1440 days and the
model means it.
Getting started
$ stack build
$ stack test
$ stack run # a small demo: one card, graded Good ten times
Using it
import FSRS
-- Grade a brand-new card Good, then grade it again a week later.
firstReview, secondReview :: MemoryState
firstReview = nextMemoryState defaultParameters Nothing 0 Good
secondReview = nextMemoryState defaultParameters (Just firstReview) 7 Good
-- When should it come back, if we want a 90% chance of recall?
whenDue :: Days
whenDue = nextIntervalDays defaultParameters 0.9 (memoryStability secondReview)
-- How likely are we to recall it three days from now?
odds :: Retrievability
odds = retrievability defaultParameters 3 (memoryStability secondReview)
Whole-card scheduling — learning steps, due dates, lapses, fuzz — lives in
FSRS.Scheduler:
import FSRS
session :: UTCTime -> (Card, ReviewLog)
session now = reviewCard defaultScheduler (newCard now) Good now
reviewCard is deterministic. If you want Anki-style interval fuzzing, use
reviewCardFuzzed and hand it the random sample yourself, so scheduling stays
a pure function of its inputs.
Optimising the 35 weights against a user's own review history is not part of
this package. Use the upstream optimiser and feed the result to mkParameters.
Modules
| Module |
What is in it |
FSRS |
Re-exports everything below. |
FSRS.Types |
Rating, MemoryState, the type synonyms. |
FSRS.Parameters |
The 35 weights, their bounds, validation, typed views onto the blocks. |
FSRS.Algorithm |
The model: forgetting curve, difficulty, stability, interval inversion. |
FSRS.Scheduler |
Cards, due dates, learning steps, fuzz. |
Provenance
FSRS.Algorithm is a transcription of the reference implementation the
upstream authors benchmark against:
srs-benchmark,
models/fsrs_v7.py and models/fsrs_v7_interval_penalty.py (revision
8c11619).
Two things are worth knowing about the default weights:
- The published defaults use 1.3 for
w15 and w24, the easy bonus of the
two stability blocks. The Default Parameters section of the srs-benchmark
README still lists 1.15; that block has not been touched since 2026-03-18,
while the model itself was changed to 1.3 three days later (commit
e274ac3). This package follows the model.
- The parameter bounds in
parameterBounds come from the clipper the upstream
optimiser applies after every gradient step, so any weights a real optimiser
produces will satisfy them.
The scheduling policy in FSRS.Scheduler is not specified upstream — only
the memory model is. It follows the reference scheduler from
py-fsrs, adapted to
FSRS-7's continuous intervals.
Tests
Two complementary suites, 113 test cases in all:
- Golden vectors — 2,589 of them, covering every function of the model
across three parameter sets, generated by
reference/fsrs7_reference.py, a
pure-Python transcription of the same upstream source. Both implementations
perform the same floating-point operations in the same order, so they are
checked to a relative tolerance of 1e-12.
- Properties — invariants that should hold for every parameter vector
inside the valid box: retrievability is a probability and decreases with
time, a better rating never means less stability, difficulty stays in range
however long the history, the interval solver really does land on the desired
retention, and so on.
A few properties hold only for well-behaved weights and say so. Because
FSRS-7 re-weights its two power laws by stability, adversarial-but-in-bounds
weights can make retrievability fall as stability grows; the properties about
how the model responds to stability are therefore stated for
defaultParameters.
To regenerate the golden vectors after touching the reference:
$ python3 reference/gen_golden.py
The reference gets two checks of its own, both standard-library only:
$ python3 reference/test_reference.py # the model's invariants, in Python
$ python3 reference/check_golden.py # the committed vectors still match it
check_golden.py compares numerically rather than by git diff. exp and
pow are not required by IEEE-754 to be correctly rounded, so the last bit of
a literal can legitimately differ between the machine that generated the file
and the one checking it; a textual diff would go red for reasons that have
nothing to do with the model.
Continuous integration
.github/workflows/ci.yml builds and tests with
Stack under --pedantic (-Wall -Werror), smoke-tests the demo, and runs both
reference checks.
Licence
MIT. See LICENSE.