# haskell-fsrs
[](https://github.com/kutyel/haskell-fsrs/actions/workflows/ci.yml)
A Haskell implementation of **FSRS-7**, the seventh version of the [Free Spaced
Repetition Scheduler](https://github.com/open-spaced-repetition) — 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
```console
$ stack build
$ stack test
$ stack run # a small demo: one card, graded Good ten times
```
## Using it
```haskell
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`:
```haskell
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`](https://github.com/open-spaced-repetition/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`](https://github.com/open-spaced-repetition/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:
```console
$ python3 reference/gen_golden.py
```
The reference gets two checks of its own, both standard-library only:
```console
$ 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`](.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](LICENSE).