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.
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:
setNewStepssetGraduatingIntervalsetEasyIntervalsetStartingEasesetEasyBonussetIntervalModifiersetMaximumIntervalsetHardIntervalsetLapseStepssetLapseNewIntervalsetLapseMinimumIntervalsetLeechThresholdAn example follows:
createSettings
|> setNewSteps [ 1, 2, 3 ]
|> setLapseSteps [ 4, 5, 6 ]
|> setLeechThreshold 0setNewSteps 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 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 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 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 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 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 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 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. 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 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 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 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 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 Cards and Decks. In simple terms, a Card may be thought of as a single flashcard and a Deck as a list or collection of Cards.
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 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
}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 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 strA Json encoder/decoder is also provided for AnkiSettings, since a Deck's settings must be preserved between sessions.
encoderSRSData provides a Json encoder for encoding SRSData from a Card.
decoderSRSData provides a Json decoder for decoding SRSData for a Card.
encoderAnkiSettings provides a Json encoder for encoding AnkiSettings from a Deck.
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. Should not be presented as an option when answering cards in the Learning queue. In the Learning queue, Hard is merely a synonym for Again.
Good -- A correct response of appropriate difficulty.
Easy -- A correct response that was excessively easy to produce (will increase ease and interval faster)
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.
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 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, as well as whether or not they are leeches (as List (Int, Bool)). The returned indices will be sorted in the following order:
getDueCardIndices will show cards up to 20 minutes early, as per Anki.
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 more lapses
Cards with fewer lapses
AnkiSettingscustomizes the behavior of this algorithm. Refer to this section of the Anki manual for more details. It may be created with thecreateSettingsfunction and its values may be changed form default by piping to the following functions:New Cards
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. AnsweringAgainorHardwill return a card to step 0. AnsweringEasywill instantly graduate a card. AnsweringGoodwill advance the card 1 step. If this list is empty, cards will instantly graduate the first time they are reviewed, regardless of theAnswer. 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. IfnewStepsis 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 ofEasy. This value is not used ifnewStepsis 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).Reviews
setEasyBonus : Float -> AnkiSettings -> AnkiSettings-- Anki default 1.3 Sets the multiplier for the next interval of a card that was answeredEasy(i.e. additional interval length over merely answeringGood. 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 whenHardis 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 ofhardIntervalthat would result in a longer interval than that from answeringGood(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.Lapses
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. AnsweringAgainorHardwill return a card to step 0. AnsweringEasywill instantly graduate a card back to review. AnsweringGoodwill advance the card 1 step. If this list is empty, cards will instantly graduate the first time they are reviewed, regardless of theAnswer.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."getDueCardIndiceswill return the leech status of each card in the deck. Additionally,getLeecheswill return all leeches in a deck (regardless of due status). Setting this value to less than or equal to 0 turns off leeches entirely.