haskell-fsrs
FSRS-7, the Free Spaced Repetition Scheduler
https://github.com/kutyel/haskell-fsrs#readme
| Stackage Nightly 2026-10-08: | 7.1.0 |
| Latest on Hackage: | 7.1.0 |
haskell-fsrs-7.1.0@sha256:e529f5407032f7aee49440f351e4c962f28f42619c980b59c98a785ae2363a66,3089Module documentation for 7.1.0
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 three 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;
- a second, faster-decaying stability — FSRS-7 tracks two memory traces, which is how it tells a review minutes later apart from one weeks later;
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 34 parameters, up from FSRS-6’s 21. What changed:
- A card has two memory traces, not one. Alongside the familiar stability there is a second, faster-decaying one, and each is updated by its own block of eight weights — the slow trace from the mixed retrievability, the fast trace from its own. This is what makes same-day reviews behave differently from spaced ones, where FSRS-6 switched between two formulas on a same-day / not-same-day flag.
- The forgetting curve is a mixture of two power laws rather than one, one per trace, with the mixing weights depending on the traces’ own stabilities. The curve has no closed-form inverse, so computing an interval is a root-find, not a formula.
- The forgetting curve depends on difficulty, which it never did in any earlier version: a hard card experiences time faster, and leans more on its slow trace.
- Post-lapse stability no longer depends on difficulty, and a lapse’s difficulty step is weighted by how surprising the lapse was — forgetting a card you were expected to recall says more about it than forgetting one that was long overdue.
- 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 / 1440days and the model means it.
One consequence worth knowing: retrievability is rescaled into
[1e-5, 1 - 1e-5], so a card reviewed a moment ago has a retrievability just
short of 1 rather than exactly 1.
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 secondReview
-- How likely are we to recall it three days from now?
odds :: Retrievability
odds = retrievability defaultParameters 3 secondReview
Both take the whole MemoryState, not just a stability: the FSRS-7 curve mixes
the two traces and is shaped by the difficulty too.
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 34 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 34 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 finished FSRS-7, whose reference
implementation is the Rust model the algorithm’s author signed off on:
Expertium/fsrs-rs-speed-autoresearch
(fsrs-rs/src/model.rs, PARAM_LEN = 34), upstreamed into fsrs-rs in
PR #426.
Two things are worth knowing about the weights:
- Indices 31–33 follow the reference’s all-positive storage convention: they
are stored shifted so that their range starts at zero, and the formulas
offset them back (
w - 0.5for the difficulty weight,w - 0.3for the two decay modulators).parametersToListgives you the stored values, which is what an upstream optimiser produces and consumes. - The parameter bounds in
parameterBoundscome 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
Three complementary suites, 126 test cases in all:
- Golden vectors — 5,586 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 of1e-12. - Upstream fixtures — the two numeric assertions the reference
implementation makes about FSRS-7 in its own test suite
(
test_memory_state_fsrs7andtest_next_interval_fsrs7), reproduced verbatim. These are the only fixtures in the package that did not come out of our own transcription, so they are what pins the port to upstream rather than to itself. - 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, post-lapse stability ignores difficulty, 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. Two kinds of caveat show up:
- Because FSRS-7 re-weights its two power laws by stability,
adversarial-but-in-bounds weights can make retrievability fall as stability
grows. Those properties are stated for
defaultParameters. - Difficulty pulls the curve two ways — it speeds the slow component up, but also shifts mixing weight onto the fast one. At the trace ratio the model itself maintains the first effect wins; for an arbitrarily lopsided pair of traces neither does. Properties about difficulty and about stability are therefore stated over states at that ratio.
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.
Changes
Changelog for haskell-fsrs
All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog.
The major version tracks the FSRS algorithm version, as py-fsrs and fsrs-rs
do: 7.x.y implements FSRS-7.
Unreleased
7.1.0 - 2026-08-21
Track the finished FSRS-7. Release 7.0.0 implemented the 35-weight,
single-trace draft still published as models/fsrs_v7.py in srs-benchmark;
the algorithm was subsequently finalised with 34 weights and a dual-trace
memory model, and that is what “FSRS-7” now means. The reference is
Expertium/fsrs-rs-speed-autoresearch,
upstreamed into fsrs-rs in
PR #426.
This is a breaking change to the model’s numbers and to several signatures. Stored parameter vectors and stored memory states from 7.0.0 are not compatible.
Changed
parameterCountis now34, anddefaultParameters,parameterBoundsandorderingConstraintsare the finished model’s. The layout is:w0..w3initial stability,w4..w6difficulty,w7..w14slow-trace stability,w15..w22fast-trace stability,w23..w30forgetting curve,w31..w33the curve’s modulation by difficulty and stability.MemoryStategained a third field,memoryStabilityFast: FSRS-7 tracks two memory traces. A new card’s fast trace starts atfastTraceRatio(0.8) of its slow one.retrievability,retrievabilityDerivativeandnextIntervalDaystake a wholeMemoryStateinstead of aStability— the curve mixes both traces and is shaped by the difficulty too. So doesnextReviewInterval.retrievabilityis rescaled into[1e-5, 1 - 1e-5], soretrievability p 0is now just short of1rather than exactly1.nextDifficultytakes the retrievability at review time: a lapse’s step is weighted by how surprising the lapse was.stabilityAfterReviewtakes a stability and a difficulty rather than aMemoryState, since it now applies to whichever trace is being updated, and its post-lapse branch no longer depends on difficulty.nextStabilityreturns both traces as a(slow, fast)pair.longTermWeights/shortTermWeightsare nowslowTraceWeights/fastTraceWeights, andStabilityWeightslostswFailureDifficultyExponent— the weight it read was ablated from both blocks.CurveWeightswas reshaped:cwDecay1becamecwDecayBase1(the fast component’s decay is now scaled by the fast stability, so it is not a constant),cwDecay2holds a magnitude rather than a negated value, andcwDifficultyWeight,cwDifficultyDecayandcwStabilityDecay1are new.
Added
fastTraceRetrievability— the fast trace’s own recall probability, which is what drives its stability update.initialMemoryState— the state a card is born with.retrievabilityFloorandfastTraceRatio.orderingConstraintsis now exported fromFSRS.Parameters.- The two numeric assertions the reference implementation makes about FSRS-7 in its own test suite are now reproduced in ours, in Haskell and in Python, so the port is pinned to upstream and not only to our own transcription.
Removed
transitionCoefficient,transitionRateandtransitionAmplitude, along with the two weights behind them. The elapsed-time blend between a long- and a short-term stability update is gone; the fast memory trace replaces it.
7.0.0 - 2026-08-20
Initial release: FSRS-7.
Added
FSRS.Types—Rating,MemoryStateand theStability/Difficulty/Retrievability/Dayssynonyms.FSRS.Parameters— the 35 FSRS-7 weights (of the draft model, see 7.1.0), the default set, bounds taken from the upstream optimiser’s clipper, validation with per-weight errors, clamping, and typed views onto the weight blocks (StabilityWeights,CurveWeights).FSRS.Algorithm— the model: the two-componentretrievabilitycurve and its derivative,nextIntervalDays(a safeguarded Newton root-find, since the FSRS-7 curve has no closed-form inverse),initialDifficulty,nextDifficulty,initialStability,stabilityAfterReview,transitionCoefficient,nextStability,nextMemoryStateandreplayReviews.FSRS.Scheduler— cards, due dates, learning and relearning steps, review logs, interval preview and explicit (pure) interval fuzzing.FSRS— an umbrella module re-exporting all of the above.- A test suite of golden vectors generated from a pure-Python transcription of the upstream reference implementation, plus property tests for the model’s invariants.