Tutorial Quiz Questions in `learnr`

library(learnr)
knitr::opts_chunk$set(echo = TRUE, highlight = TRUE)

Overview

You can include one or more multiple-choice quiz questions within a tutorial to help verify that readers understand the concepts presented. Basic questions can either have one or more correct answers.

Include a question by calling the question function within an R code chunk:

```r`r ''`
question("What number is the letter A in the English alphabet?",
  answer("8"),
  answer("14"),
  answer("1", correct = TRUE),
  answer("23")
)
```

The above example defines a question with a single correct answer. You can also create questions that require multiple answers to be specified:

```r`r ''`
question("Where are you right now? (select ALL that apply)",
  answer("Planet Earth", correct = TRUE),
  answer("Pluto"),
  answer("At a computing device", correct = TRUE),
  answer("In the Milky Way", correct = TRUE),
  incorrect = "Incorrect. You're on Earth, in the Milky Way, at a computer."
)
```

Note that for the examples above we specify the echo = FALSE option on the R code chunks that produce the questions. This is required to ensure that the R source code for the questions is not printed within the document.

This is what the above example quiz questions would look like within a tutorial:

question("What number is the letter A in the English alphabet?",
  answer("8"),
  answer("14"),
  answer("1", correct = TRUE),
  answer("23")
)
question("Where are you right now? (select ALL that apply)",
  answer("Planet Earth", correct = TRUE),
  answer("Pluto"),
  answer("At a computing device", correct = TRUE),
  answer("In the Milky Way", correct = TRUE),
  incorrect = "Incorrect. You're on Earth, in the Milky Way, at a computer."
)

Custom Messages

You can add answer-specific correct/incorrect messages using the message option. For example:

```r`r ''`
question("What number is the letter A in the *English* alphabet?",
  answer("8"),
  answer("1", correct = TRUE),
  answer("2", message = "2 is close but it's the letter B rather than A."),
  answer("26")
)
```
question("What number is the letter A in the *English* alphabet?",
  answer("8"),
  answer("1", correct = TRUE),
  answer("2", message = "2 is close but it's the letter B rather than A."),
  answer("26")
)

Formatting and Math

You can use markdown to format text within questions, answers, and custom messages. You can also include embedded LaTeX math using the $ delimiter. For example:

```r`r ''`
x <- 42
question(sprintf("Suppose $x = %s$. Choose the correct statement:", x),
  answer(sprintf("$\\sqrt{x} = %d$", x + 1)),
  answer(sprintf("$x ^ 2 = %d$", x^2), correct = TRUE),
  answer("$\\sin x = 1$")
)
```

Note the use of a double-backslash (\\) as the prefix for LaTeX macros. This is necessary to “escape” the single-backslash so that R doesn’t interpret it as a special character. Here’s what this example would look like within a tutorial:

x <- 42
question(sprintf("Suppose $x = %s$. Choose the correct statement:", x),
  answer(sprintf("$\\sqrt{x} = %d$", x + 1)),
  answer(sprintf("$x ^ 2 = %d$", x^2), correct = TRUE),
  answer("$\\sin x = 1$")
)

Retrying Questions

By default when an incorrect answer is provided users get the appropriate feedback and the correct answer(s) are highlighted. You can also provide an option for the user to try the question again. You can do this using the allow_retry option, for example:

```r`r ''`
question("What number is the letter A in the English alphabet?",
  answer("8"),
  answer("14"),
  answer("1", correct = TRUE),
  answer("23"),
  allow_retry = TRUE
)
```
question("What number is the letter A in the English alphabet?",
  answer("8"),
  answer("14"),
  answer("1", correct = TRUE),
  answer("23"),
  allow_retry = TRUE
)

Random Answer Order

If you want the answers to questions to be randomly arranged, you can add the random_answer_order option. For example:

```r`r ''`
question("What number is the letter A in the English alphabet?",
  answer("8"),
  answer("14"),
  answer("1", correct = TRUE),
  answer("23"),
  random_answer_order = TRUE
)
```
question("What number is the letter A in the English alphabet?",
  answer("8"),
  answer("14"),
  answer("1", correct = TRUE),
  answer("23"),
  random_answer_order = TRUE
)

Groups of Questions

You can present a group of related questions as a quiz by wrapping your questions within the quiz function. For example:

```r`r ''`
quiz(caption = "Quiz 1",
  question("What number is the letter A in the *English* alphabet?",
    answer("8"),
    answer("14"),
    answer("1", correct = TRUE),
    answer("23")
  ),
  question("Where are you right now? (select ALL that apply)",
    answer("Planet Earth", correct = TRUE),
    answer("Pluto"),
    answer("At a computing device", correct = TRUE),
    answer("In the Milky Way", correct = TRUE),
    incorrect = "Incorrect. You're on Earth, in the Milky Way, at a computer."
  )
)
```
quiz(caption = "Quiz 1",
  question("What number is the letter A in the *English* alphabet?",
    answer("8"),
    answer("14"),
    answer("1", correct = TRUE),
    answer("23")
  ),
  question("Where are you right now? (select ALL that apply)",
    answer("Planet Earth", correct = TRUE),
    answer("Pluto"),
    answer("At a computing device", correct = TRUE),
    answer("In the Milky Way", correct = TRUE),
    incorrect = "Incorrect. You're on Earth, in the Milky Way, at a computer."
  )
)

Basic Question Types

There are four basic types of quiz questions: radio button, checkbox, text box, and numeric. Each one allows for one choice, multiple choices, and direct user text or numeric input respectively.


Radio

When wanting only one answer from a user, use a radio button question. Even if multiple options are correct, the user is only able to pick a single value.

```r`r ''`
question_radio(
  "Is this a good question?",
  answer("yes", correct = TRUE),
  answer("no", message = 'This is a good question.')
)
```
question_radio(
  "Is this a good question?",
  answer("yes", correct = TRUE),
  answer("no", message = 'This is a good question.')
)

Checkbox

When wanting possibly multiple answers from a user, use a checkbox question. All answers that are marked correct must be selected by the user to have the answer be correct. A minimum of one correct answer is required.

```r`r ''`
question_checkbox(
  "Select all the toppings that belong on a Margherita Pizza:",
  answer("tomato", correct = TRUE),
  answer("mozzarella", correct = TRUE),
  answer("basil", correct = TRUE),
  answer("extra virgin olive oil", correct = TRUE),
  answer("pepperoni", message = "Great topping! ... just not on a Margherita Pizza"),
  answer("onions"),
  answer("bacon"),
  answer("spinach"),
  random_answer_order = TRUE,
  allow_retry = TRUE,
  try_again = "Be sure to select all toppings!"
)
```
question_checkbox(
  "Select all the toppings that belong on a Margherita Pizza:",
  answer("tomato", correct = TRUE),
  answer("mozzarella", correct = TRUE),
  answer("basil", correct = TRUE),
  answer("extra virgin olive oil", correct = TRUE),
  answer("pepperoni", message = "Great topping! ... just not on a Margherita Pizza"),
  answer("onions"),
  answer("bacon"),
  answer("spinach"),
  random_answer_order = TRUE,
  allow_retry = TRUE,
  try_again = "Be sure to select all toppings!"
)

Text box

If you'd like users to submit open-ended answers, use a text box question. Correct and incorrect answers will be matched exactly (including white space). A minimum of one correct answer is required.

Text value inputs are trimmed before they are compared to answers provided.

```r`r ''`
question_text(
  "Please enter the word 'C0rrect' below:",
  answer("correct", message = "Don't forget to capitalize"),
  answer("c0rrect", message = "Don't forget to capitalize"),
  answer("Correct", message = "Is it really an 'o'?"),
  answer("C0rrect", correct = TRUE),
  allow_retry = TRUE
)
```
question_text(
  "Please enter the word 'C0rrect' below:",
  answer("correct", message = "Don't forget to capitalize"),
  answer("c0rrect", message = "Don't forget to capitalize"),
  answer("Correct", message = "Is it really an 'o'?"),
  answer("C0rrect", correct = TRUE),
  allow_retry = TRUE
)

Numeric

If you'd like users to supply numeric answers, use question_numeric(). A minimum of one correct answer is required.

You can provide an initial value and a step size for the numeric input. Note that the step size only affects the numeric input when the up/down arrows are used to increment/decrement the number; users can still manually enter numbers that don't follow the step size.

Use min and max to set the minimum and maximum values. If a user submits a value outside of the min/max bounds, the feedback message will reveal the lower or upper bound.

```r`r ''`
question_numeric(
 "What is pi rounded to 2 digits?",
 answer(3, message = "Don't forget to use the digits argument"),
 answer(3.1, message = "Too few digits"),
 answer(3.142, message = "Too many digits"),
 answer(3.14, correct = TRUE),
 allow_retry = TRUE,
 min = 3,
 max = 4,
 step = 0.01
)
```
question_numeric(
 "What is pi rounded to 2 digits?",
 answer(3, message = "Don't forget to use the digits argument"),
 answer(3.1, message = "Too few digits"),
 answer(3.142, message = "Too many digits"),
 answer(3.14, correct = TRUE),
 allow_retry = TRUE,
 min = 3,
 max = 4,
 step = 0.01
)

Extra Arguments

All the the arguments described below are also accepted by question_radio, question_checkbox, question_rank, and the general question methods.

Example:

```r`r ''`
question(
  "How many parameters are supplied in this question?",
  answer("1", message = "Really?"),
  answer("2"),
  answer("3"),
  answer("15", correct = TRUE, message = "Custom message here."),
  type = "learnr_text", # radio
  correct = "Question is correct!",
  try_again = "Try Again!",
  message = "Reminder to do something after answering the question...",
  post_message = "Message to be displayed after the correct answer is found",
  submit_button = "Submit Answer Here!",
  try_again_button = "Try Again Here!",
  allow_retry = TRUE,
  random_answer_order = TRUE,
  options = list(
    placeholder = "The answer is '15'",
    trim = TRUE
  )
)
```
question(
  "How many parameters are supplied in this question?",
  answer("1", message = "Really?"),
  answer("2"),
  answer("3"),
  answer("15", correct = TRUE, message = "Custom message here."),
  type = "learnr_text", # radio
  correct = "Question is correct!",
  try_again = "Try Again!",
  message = "Reminder to do something after answering the question...",
  post_message = "Message to be displayed after the correct answer is found",
  submit_button = "Submit Answer Here!",
  try_again_button = "Try Again Here!",
  allow_retry = TRUE,
  random_answer_order = TRUE,
  options = list(
    placeholder = "The answer is '15'",
    trim = TRUE
  )
)

Custom Question Types

learnr comes with four built in question types explained in Basic Question Types: question_radio, question_checkbox, question_text, and question_numeric. Similar to ggplot2 not being able to implement every possible geom, learnr cannot implement every possible question type.

To address this, learnr utilizes five generic S3 methods to define the behavior of a custom tutorial question. Each generic S3 method should correspond to one of type values supplied to the question.

If a vector of types is supplied to a question's type, the S3 methods will be dispatched starting with the first type value and ending with the last type value. For example, let us define a new question type that should behave like a learnr checkbox question, but have a special implementation of question_is_correct where the answer is always correct.

```r`r ''`
question_is_correct.always_correct <- function(question, value, ...) {
  learnr::mark_as(TRUE, messages = NULL)
}

registerS3method("question_is_correct", "always_correct", question_is_correct.always_correct)

ques <- question(
  "Custom Method",
  answer("answer", correct = TRUE),
  answer("also marked as correct", correct = FALSE),
  type = c("always_correct", "learnr_checkbox")
)
```
question_is_correct.always_correct <- function(question, value, ...) {
  learnr::mark_as(TRUE, messages = NULL)
}

registerS3method("question_is_correct", "always_correct", question_is_correct.always_correct)

ques <- question(
  "Custom Method",
  answer("answer", correct = TRUE),
  answer("also marked as correct", correct = FALSE),
  type = c("always_correct", "learnr_checkbox")
)
ques

When the question, ques, above is initialized, question_ui_initialize will dispatch on question_ui_initialize.always_correct. Since no method has been defined, question_ui_initialize.learnr_checkbox will be called. However, when question_is_correct is called, question_is_correct.always_correct is found and called.

Supplying multiple type values allows for clean abstraction between objects that share common functionality.

sortable

sortable, an htmlwidget wrapper to SortableJS (a "JavaScript library for reorderable drag-and-drop lists."), as implemented a custom question type: question_rank. The convention of naming the new question method as question_METHOD will help users find your newly defined question function by namespacing it with question_.

sortable defined it's ranking question as question_rank. It uses the options parameter to pass along the output of sortable_options, which are specific to the SortableJS library. By naming the type "sortable_rank", the type is namespaced by the package while still including a sub type. By making a wrapper function, different default question parameters can be implemented, such as setting random_answer_order to be TRUE.

question_rank <- function(..., random_answer_order = TRUE, options = sortable::sortable_options()) {
  learnr::question(
    ...,
    random_answer_order = random_answer_order,
    type = "sortable_rank",
    options = options
  )
}
```r`r ''`
rank_ex <- sortable::question_rank(
  "Sort the first 5 letters",
  learnr::answer(LETTERS[1:5], correct = TRUE),
  learnr::answer(rev(LETTERS[1:5]), correct = FALSE, "Other direction!")
)
rank_ex
```
rank_ex <- sortable::question_rank(
  "Sort the first 5 letters",
  learnr::answer(LETTERS[1:5], correct = TRUE),
  learnr::answer(rev(LETTERS[1:5]), correct = FALSE, "Other direction!")
)
rank_ex

In the code sections below, we will walk through each method and a final helper method to create a sortable_rank tutorial question.

question_ui_initialize.sortable_rank

question_ui_initialize() is called when the question is first initialized and after a user wants to try again.

The code below extracts the necessary information to create a Shiny UI object, using the the question$ids$answer value and labels from the first answer. Using the learnr::question options parameter, we can pass options directly to the sortable::sortable_js function.

question_ui_initialize.sortable_rank <- function(question, value, ...) {

  if (!is.null(value)) {
    # if an answer exists already, it should be displayed as is
    labels <- value
  } else {
    # get the first answer
    labels <- question$answers[[1]]$option

    # if we should randomize the order
    if (
      isTRUE(question$random_answer_order)
    ) {
      # shuffle the options
      labels <- sample(labels, length(labels))
    }
  }

  # return the sortable htmlwidget
  sortable::rank_list(
    text = question$question,
    input_id = question$ids$answer,
    labels = labels,
    options = question$options
  )
}

Example:

# initial state with no initial value
question_ui_initialize(rank_ex, value = NULL)

question_ui_completed.sortable_rank

question_ui_completed() is called when the question is finished and cannot be tried again.

The code below adds an extra options value to disable SortableJS. Like question_ui_initialize, it also returns a Shiny UI object.

question_ui_completed.sortable_rank <- function(question, value, ...) {
  question$options <- modifyList(
    question$options,
    # forcefully add an extra option to disable sortable.js
    sortable::sortable_options(disabled = TRUE)
  )
  # disable all html tags
  learnr::disable_all_tags(
    # display just like init, with current answer
    learnr::question_ui_initialize(question, value, ...)
  )
}

Example:

# completed state with display value set to C, A, D, E, B
question_ui_completed(rank_ex, value = c("C", "A", "D", "E", "B"))

question_ui_try_again.sortable_rank

The sortable package chose to have the "Try Again" state be defined the same as the "Completed" state.

question_ui_try_again.sortable_rank <- question_ui_completed.sortable_rank

question_is_valid.sortable_rank

question_is_valid() is called to determine if the submit button should be clickable.

The default value of !is.null(value) will work for the sortable question. Therefore, no implementation is needed.

question_is_correct.sortable_rank

question_is_correct() is called to determine if the answer submitted is correct. This method is required and it may be helpful to use the options supplied to a question to change how an answer is found to be correct.

Since sortable is comparing a character vector to possible answer character vectors, a quick identical check through each answer will be enough for this method.

The return value from question_is_correct should return output from mark_as. This allows for extra messages to be displayed while determining if the answer is correct / incorrect.

question_is_correct.sortable_rank <- function(question, value, ...) {
  # for each possible answer, check if it matches
  for (answer in question$answers) {
    if (identical(answer$option, value)) {
      # if it matches, return the correct-ness and its message
      return(learnr::mark_as(answer$is_correct, answer$message))
    }
  }
  # no match found. mark as incorrect
  return(learnr::mark_as(FALSE, NULL))
}

Example:

# with incorrect values
question_is_correct(rank_ex, value = c("E", "D", "C", "B", "A"))

# with correct values
question_is_correct(rank_ex, value = c("A", "B", "C", "D", "E"))


Try the learnr package in your browser

Any scripts or data that you put into this service are public.

learnr documentation built on Sept. 28, 2023, 9:06 a.m.