This package provides everything necessary to create spaced repetition software using the algorithm used by the popular F/OSS program Anki. Anki's algorithm is a heavily-modified version of the SM-2 algorithm, which has been released for free public use when accompanied by the following notice:
Algorithm SM-2, (C) Copyright SuperMemo World, 1991.
For details about Anki's algorithm, please refer to the following section of its manual.
The above description details how Anki's algorithm differs from the SM-2 algorithm, but briefly, the following differences may be noted:
SM-2 defines an initial interval of 1 day then 6 days, whereas Anki's algorithm allows for customization of these initial intervals.
Anki's algorithm uses 4 choices (3 during learning) for answering cards, not 6.
Answering cards later than scheduled will be factored into the next interval calculation.
Failing a review card may cause behavior besides fully resetting it (if desired).
"Easy" answer choice increases the next interval in addition to ease/E-factor.
Successive failures while cards are in learning do not result in further decreases to the card’s ease.
This algorithm requires certain settings be provided when functions are called to specify the behavior of the system. A builder pattern is available for creating settings with defaults.
{ newSteps : List (TimeInterval Minutes)
, graduatingInterval : TimeInterval Days
, easyInterval : TimeInterval Days
, startingEase : Basics.Float
, easyBonus : Basics.Float
, intervalModifier : Basics.Float
, maximumInterval : TimeInterval Days
, hardInterval : Basics.Float
, lapseSteps : List (TimeInterval Minutes)
, lapseNewInterval : Basics.Float
, lapseMinimumInterval : TimeInterval Days
, leechThreshold : Basics.Int
}
AnkiSettings
customizes the behavior of this algorithm. Refer to this section of the Anki manual for more details. It may be created with the createSettings
function and its values may be changed form default by piping to the following functions:
setNewSteps : List Int -> AnkiSettings -> AnkiSettings
-- Anki default: [1, 10] Sets the interval steps (in minutes) that a card must go through while in the learning process. Answering Again
or Hard
will return a card to step 0. Answering Easy
will instantly graduate a card. Answering Good
will advance the card 1 step. If this list is empty, cards will instantly graduate the first time they are reviewed, regardless of the Answer
. Note that an interval must be at least 1 minute long; any value less than 1 minute will result in an interval of 1 minute.
setGraduatingInterval : Int -> AnkiSettings -> AnkiSettings
-- Anki default: 1 Sets the initial interval (in days) a card will be scheduled for upon graduating from the learning phase. If newSteps
is empty, all new cards will be scheduled for this interval when first encountered, regardless of answer. Note that this interval will not be fuzzed, per Anki's source. Values less than 1 will result in 1 being used.
setEasyInterval : Int -> AnkiSettings -> AnkiSettings
-- Anki default: 4 Sets the initial interval (in days) a card will be scheduled for upon instantly graduating from the learning phase with an answer of Easy
. This value is not used if newSteps
is empty. Note that this interval will not be fuzzed, per Anki's source. Values less than 1 will result in 1 being used.
setStartingEase : Float -> AnkiSettings -> AnkiSettings
-- Anki default: 2.5 Sets the initial ease that a card graduating from learning will have. Values less than 1.3 are ignored and result in 1.3 being used instead (as that is the minimum ease a card may have).
setEasyBonus : Float -> AnkiSettings -> AnkiSettings
-- Anki default 1.3 Sets the multiplier for the next interval of a card that was answered Easy
(i.e. additional interval length over merely answering Good
. Values less than 1.0 are ignored.
setIntervalModifier : Float -> AnkiSettings -> AnkiSettings
-- Anki default 1.0 Set the multiplier for all intervals, e.g. setting it to 0.8 will result in all review intervals being 80% as long as they would normally be. This multiplier may be used to increase/decrease retention at the cost of increasing/decreasing study time, respectively. Values less than or equal to zero are ignored.
setMaximumInterval : Int -> AnkiSettings -> AnkiSettings
-- Anki default: 36500 Set the upper limit on the time the algorithm will schedule a card for (in days). Small values may be used to cap time between seeing a card (to increase retention). Values less than 1 are ignored. Note that, due to the limitations of integers, an absolute maximum interval of 1491308 days (4085 years) exists. Since you almost certainly won't be alive in 4000 years to study a card, this shouldn't be a problem.
setHardInterval : Float -> AnkiSettings -> AnkiSettings
-- Anki default: 1.2 Set the multiplier applied to the previous interval when Hard
is answered to a card. Note that intervals are forced to be at least 1 day longer than the previous one (before fuzzing). As such, values <= 1 will have no effect. Additionally, values of hardInterval
that would result in a longer interval than that from answering Good
(i.e. that are larger than the ease of the card) are ignored, with ease being used instead. As such, it is probably good practice to keep this value <= 1.3, as that is the lower limit on ease.
lapseSteps : List Int -> AnkiSettings -> AnkiSettings
-- Anki default: [10] Set the interval steps (in minutes) that a card must go through while relearning after a lapse. Answering Again
or Hard
will return a card to step 0. Answering Easy
will instantly graduate a card back to review. Answering Good
will advance the card 1 step. If this list is empty, cards will instantly graduate the first time they are reviewed, regardless of the Answer
.lapseNewInterval : Float -> AnkiSettings -> AnkiSettings
-- Anki default: 0.0 Set a multiplier for the pre-lapse interval to determine the new interval after graduating a card back from re-learning. Values less than 0.0 are ignored. Note that this interval is not fuzzed, per Anki's source.lapseMinimumInterval : Int -> AnkiSettings -> AnkiSettings
-- Anki default: 1 Set a minimum bound on the previous option, with lapsed cards getting a new interval of at least this value. Answers less than 1 are ignored.leechThreshold : Int -> AnkiSettings -> AnkiSettings
-- Anki default: 8 Set the number of lapses before a card is considered a "leech." getDueCardIndices
will return the leech status of each card in the deck. Additionally, getLeeches
will return all leeches in a deck (regardless of due status). Setting this value to less than or equal to 0 turns off leeches entirely.createSettings : AnkiSettings
createSettings
creates an AnkiSettings
with the same default settings as the Anki program itself. It may be piped into the following functions to update from default:
setNewSteps
setGraduatingInterval
setEasyInterval
setStartingEase
setEasyBonus
setIntervalModifier
setMaximumInterval
setHardInterval
setLapseSteps
setLapseNewInterval
setLapseMinimumInterval
setLeechThreshold
An example follows:
createSettings
|> setNewSteps [ 1, 2, 3 ]
|> setLapseSteps [ 4, 5, 6 ]
|> setLeechThreshold 0
setNewSteps : List Basics.Int -> AnkiSettings -> AnkiSettings
setNewSteps
sets the the interval steps (in minutes) that a card must go through while in the learning process. Answering Again
or Hard
will return a card to step 0. Answering Easy
will instantly graduate a card. Answering Good
will advance the card 1 step. If this list is empty, cards will instantly graduate the first time they are reviewed, regardless of the Answer
. Note that an interval must be at least 1 minute long; any value less than 1 minute will result in an interval of 1 minute.
setGraduatingInterval : Basics.Int -> AnkiSettings -> AnkiSettings
setGraduatingInterval
sets the initial interval (in days) a card will be scheduled for upon graduating from the learning phase. If newSteps
is empty, all new cards will be scheduled for this interval when first encountered, regardless of answer. Note that this interval will not be fuzzed, per Anki's source. Values less than 1 will result in 1 being used.
setEasyInterval : Basics.Int -> AnkiSettings -> AnkiSettings
setEasyInterval
sets the initial interval (in days) a card will be scheduled for upon instantly graduating from the learning phase with an answer of Easy
. This value is not used if newSteps
is empty. Note that this interval will not be fuzzed, per Anki's source. Values less than 1 will result in 1 being used.
setStartingEase : Basics.Float -> AnkiSettings -> AnkiSettings
setStartingEase
sets the initial ease that a card graduating from learning will have. Values less than 1.3 are ignored and result in 1.3 being used instead (as that is the minimum ease a card may have).
setEasyBonus : Basics.Float -> AnkiSettings -> AnkiSettings
setEasyBonus
sets the multiplier for the next interval of a card that was answered Easy
(i.e. additional interval length over merely answering Good
. Values less than 1.0 are ignored.
setIntervalModifier : Basics.Float -> AnkiSettings -> AnkiSettings
setIntervalModifier
sets the multiplier for all intervals, e.g. setting it to 0.8 will result in all review intervals being 80% as long as they would normally be. This multiplier may be used to increase/decrease retention at the cost of increasing/decreasing study time, respectively. Values less than or equal to zero are ignored.
setMaximumInterval : Basics.Int -> AnkiSettings -> AnkiSettings
setMaximumInterval
sets the upper limit on the time the algorithm will schedule a card for (in days). Small values may be used to cap time between seeing a card (to increase retention). Values less than 1 are ignored. Note that, due to the limitations of integers, an absolute maximum interval of 1491308 days (4085 years) exists. Since you almost certainly won't be alive in 4000 years to study a card, this shouldn't be a problem.
setHardInterval : Basics.Float -> AnkiSettings -> AnkiSettings
setHardInterval
sets the multiplier applied to the previous interval when Hard
is answered to a card. Note that intervals are forced to be at least 1 day longer than the previous one (before fuzzing). As such, values <= 1 will have no effect. Additionally, values of hardInterval
that would result in a longer interval than that from answering Good
(i.e. that are larger than the ease of the card) are ignored, with ease being used instead. As such, it is probably good practice to keep this value <= 1.3, as that is the lower limit on ease.
setLapseSteps : List Basics.Int -> AnkiSettings -> AnkiSettings
setLapseSteps
sets the interval steps (in minutes) that a card must go through while relearning after a lapse. Answering Again
or Hard
will return a card to step 0. Answering Easy
will instantly graduate a card back to review. Answering Good
will advance the card 1 step. If this list is empty, cards will instantly graduate the first time they are reviewed, regardless of the Answer
.
setLapseNewInterval : Basics.Float -> AnkiSettings -> AnkiSettings
setLapseNewInterval
sets a multiplier for the pre-lapse interval to determine the new interval after graduating a card back from re-learning. Values less than 0.0 are ignored Note that this interval is not fuzzed, per Anki's source.
setLapseMinimumInterval : Basics.Int -> AnkiSettings -> AnkiSettings
setLapseMinimumInterval
sets a minimum bound on the previous option, with lapsed cards getting a new interval of at least this value. Answers less than 1 are ignored.
setLeechThreshold : Basics.Int -> AnkiSettings -> AnkiSettings
setLeechThreshold
sets the number of lapses before a card is considered a "leech." getDueCardIndices
will return the leech status of each card in the deck. Additionally, getLeeches
will return all leeches in a deck (regardless of due status). Setting this value to less than or equal to 0 turns off leeches entirely.
The building blocks of this package are Card
s and Deck
s. In simple terms, a Card
may be thought of as a single flashcard and a Deck
as a list or collection of Card
s.
{ a | srsData : SRSData }
A Card
represents a single question or unit of knowledge the user will review. In general terms, each would represent a single flashcard. Card
is defined as an extensible record; as such, whatever necessary custom fields for a use case may simply be included in the record, e.g.:
type alias MyFlashcard =
Card { prompt : String, answer : String }
A Card
contains only the information necessary for scheduling and nothing else; all other information should be added as in the above example.
{ a | cards : Array (Card b)
, settings : AnkiSettings
}
A Deck
represents a list of cards to be studied (this might be called a "collection" in other software). It is a record with field cards
, an Array
of Card
and field settings
of AnkiSettings
. Maintaining the state of a Deck
may be handled by the user of the module or by this module itself. In general, it is probably best not to add a massive quantity of new (unstudied) cards to a deck at once.
A Card
may be created by use of the newSRSData
function, as in the following example:
type alias MyFlashcard =
Card { prompt : String, answer : String }
myFlashcard : MyFlashcard
myFlashcard =
{ prompt = "SYN"
, answer = "SYN-ACK"
, srsData = newSRSData
}
SpacedRepetition.Internal.SMTwoAnki.QueueStatus
SRSData
contains all data necessary for the Anki system and may be created with the newSRSData
function. It may additionally be saved/loaded using the Json encoder/decoder in this package
newSRSData : SRSData
newSRSData
creates a new SRSData
for inclusion in a Card
.
Since Card
data must necessarily be preserved between sessions, a Json encoder/decoder is provided for SRSData
. It may be utilized as follows:
import Json.Decode as Decode
import Json.Encode as Encode
type alias MyFlashcard =
Card { prompt : String, answer : String }
myFlashcardConstructor : SRSData -> String -> String -> MyFlashcard
myFlashcardConstructor srsData prompt answer =
{ prompt = prompt
, answer = answer
, srsData = srsData
}
myFlashcardToJson : MyFlashcard -> String
myFlashcardToJson myCard =
Encode.encode 0 <|
Encode.object
[ ( "srsData", encoderSRSData myCard.srsData )
, ( "prompt", Encode.string myCard.prompt )
, ( "answer", Encode.string myCard.answer )
]
myFlashcardDecoder : Decode.Decoder MyFlashcard
myFlashcardDecoder =
Decode.map3 myFlashcardConstructor
(Decode.field "srsData" decoderSRSData)
(Decode.field "prompt" Decode.string)
(Decode.field "answer" Decode.string)
jsonToMyFlashcard : String -> Result Decode.Error MyFlashcard
jsonToMyFlashcard str =
Decode.decodeString myFlashcardDecoder str
A Json encoder/decoder is also provided for AnkiSettings
, since a Deck
's settings must be preserved between sessions.
encoderSRSData : SRSData -> Json.Encode.Value
encoderSRSData
provides a Json encoder for encoding SRSData
from a Card
.
decoderSRSData : Json.Decode.Decoder SRSData
decoderSRSData
provides a Json decoder for decoding SRSData
for a Card
.
encoderAnkiSettings : AnkiSettings -> Json.Encode.Value
encoderAnkiSettings
provides a Json encoder for encoding AnkiSettings
from a Deck
.
decoderAnkiSettings : Json.Decode.Decoder AnkiSettings
decoderAnkiSettings
provides a Json decoder for decoding AnkiSettings
for a Deck
.
The SM-2 Anki algorithm depends on grading answers on a scale with one incorrect and three correct responses, except during the learning phase, in which case only 1 incorrect and two correct responses should be used. For some use cases, it may be able to programmatically determine the Answer
of a user's response. In other cases, however, the user may need to self-report.
The Answer
type represents how accurate/certain a user's response was to a card and must be passed to answerCard
whenever a Card
is reviewed. This package uses the same names as Anki, as presented below:
Again
-- An incorrect response.
Hard
-- A correct response that was challenging to produce. It is not necessary to present this as an option for answering cards in the Learning or Lapsed queues, as it has the same effect as Again
in those cases, namely resetting the card to the start of the queue.
Good
-- A correct response of appropriate difficulty.
Easy
-- A correct response that was excessively easy to produce (will increase ease and interval faster)
answerCardInDeck : Time.Posix -> Answer -> Basics.Int -> Deck a b -> Deck a b
answerCardInDeck
functions analogously to answerCard
but handles maintenance of the Deck
, which is typically what one would desire. When a card is presented to the user and answered, answerCardInDeck
should be called with the current time (in the Time.Posix
format returned by the now
task of the core Time
module), an Answer
, the index of the card in the Deck
, and the Deck
itself. It returns the updated Deck
. Use this function if you simply want to store a Deck
and not worry about updating it manually (which is most likely what you want). Otherwise, use answerCard
to handle updating the Deck
manually. Handling the presentation of a card is the responsibility of the implementing program, as various behaviors might be desirable in different cases. Note that if an invalid (out of bounds) index is passed, the Deck
is returned unaltered.
answerCard : Time.Posix -> Answer -> AnkiSettings -> Card a -> Card a
When a card is presented to the user and answered, answerCard
should be called with the current time (in the Time.Posix
format returned by the now
task of the core Time
module) and an Answer
. It returns the updated card, which should replace the card in the Deck
. Use this function if you want to handle updating the Deck
manually; otherwise, use answerCardInDeck
. Handling the presentation of a card is the responsibility of the implementing program, as various behaviors might be desirable in different cases.
Besides answering cards, this package handles determining which cards in a Deck
are due and require answering.
getDueCardIndices : Time.Posix -> Deck a b -> List Basics.Int
getDueCardIndices
takes the current time (in the Time.Posix
format returned by the now
task of the core Time
module) and a Deck
and returns the indices of the subset of the Deck
that is due for review. The returned indices will be sorted in the following order:
Equally due cards are presented in random order.
getDueCardIndices
will show cards up to 20 minutes early, as per Anki.
getDueCardIndicesWithDetails : Time.Posix -> Deck a b -> List { index : Basics.Int, isLeech : Basics.Bool, queueDetails : QueueDetails }
getDueCardIndicesWithDetails
takes the current time (in the Time.Posix
format returned by the now
task of the core Time
module) and a Deck
and returns the subset of the Deck
that is due for review as a list of records, providing their index, which queue they are currently in (e.g. whether they are being learned or reviewed) along with any relevant queue details, and whether or not they are leeches. The returned indices will be sorted in the following order:
Equally due cards are presented in random order.
getDueCardIndicesWithDetails
will show cards up to 20 minutes early, as per Anki.
If you require specific details for a single card, you may use the provided functionality here. If you need details for all due cards, just use getDueCardIndicesWithDetails
. You can also get all leeches using getLeeches
.
QueueDetails
represents the current status of a card.
NewCard
-- A card that has never before been studied (encountered) by the user.
LearningQueue {...}
-- A card that is in the initial learning queue, progressing through the steps specified in AnkiSettings.newSteps
.
lastSeen : Time.Posix
-- The date and time the card was last reviewed.intervalInMinutes : Int
-- The interval, in minutes from the date last seen, that the card is slated for review in.ReviewQueue {...}
-- A card that is being reviewed for retention.
lastSeen : Time.Posix
-- The date and time the card was last reviewed.intervalInDays : Int
-- The interval, in days from the date last seen, that the card was slated for review in.lapses : Int
-- The number of times the card has "lapsed," i.e. been forgotten/incorrectly answered by the user.LapsedQueue {...}
-- A card that has lapsed, i.e. one that was being reviewed but was answered incorrectly and is now being re-learned.
lastSeen : Time.Posix
-- The date and time the card was last reviewed.formerIntervalInDays : Int
-- The interval, in days from the date last seen, that the card was slated for review in prior to last being forgotten/ answered incorrectly.intervalInMinutes : Int
-- The interval, in minutes from the date last seen, that the card is slated for review in.lapses : Int
-- The number of times the card has "lapsed," i.e. been forgotten/incorrectly answered by the user.getCardDetails : AnkiSettings -> Card a -> { isLeech : Basics.Bool, queueDetails : QueueDetails }
getCardDetails
returns the current queue status for a given card and whether or not it is a leech. If you require this for every due card, simply use getDueCardIndicesWithDetails
.
getLeeches : Deck a b -> List Basics.Int
getLeeches
takes a Deck
and returns the indices of the subset of the Deck
that are leeches (as List Int
). The returned indices will be sorted in the following order:
Cards with the same number of lapses will be ordered by their appearance in the original input deck.
The following are exposed only so that they may be used in type annotations and may be created via their respective functions.
SpacedRepetition.Internal.SMTwoAnki.TimeInterval a
Opaque type. You don't need it, except maybe in type signatures.
SpacedRepetition.Internal.SMTwoAnki.Days
Opaque type. You don't need it, except maybe in type signatures.
SpacedRepetition.Internal.SMTwoAnki.Minutes
Opaque type. You don't need it, except maybe in type signatures.