Giora Simchoni
Giora Simchoni

Reputation: 3689

R: Let users use the console while inside a function, record their inputs and react

Is it possible to write a function in R which will hold its execution, giving the users control over the console (while in interactive mode of course), meanwhile recording their inputs, and continuing execution either:

Example: ask the user a question (without using readline() for the answer)

question <- function() {
  message("How much is 2 + 2?")
  #let users take control of the console
  #continue to next statement only if they input "2+2", or "4" or a minute has passed
  #meanwhile record their last input similar to ".Last.Value", e.g.:
  startTime <- Sys.time()
  timeout <- FALSE
  lastInput <- lastInput()
  while (eval(parse(text = lastInput)) != 4 & !timeout) {
    if (difftime(Sys.time(), startTime, units = "mins") > 1) {
      timeout <- TRUE
    }
    lastInput <- lastInput()
  }
  if (timeout) {
    stop("Sorry, timeout.")
  } else {
    message("Correct! Let's continue with this function:")
  }
}

Where lastInput() is a function which "listens" to user input when it changes.

Obviously the above structure is tentative and won't give me what I want, some way to "listen" or "observe" and only react when the user inputs something to the console.

The final user experience should be:

> question()
How much is 2+2?
> #I'm the user, I can do whatever
> head(mtcars)
> plot(1:10)
> 3
> 2 + 2
[1] 4
Correct! Let's continue with this function:

Am I too optimistic or is there some R magic for this?

Upvotes: 1

Views: 85

Answers (1)

Giora Simchoni
Giora Simchoni

Reputation: 3689

Thanks to @parth I have looked at swirl's source code and got acquainted with the addTaskCallback function. From the help file:

addTaskCallback registers an R function that is to be called each time a top-level task is completed.

And so we can make R check the users input ("top-level task") with a specific function, responding accordingly.

But since the swirl code is very "heavy", I think I need to supply a minimal example:

swirllike <- function(...){
  removeTaskCallback("swirllike")
  e <- new.env(globalenv())
  e$prompt <- TRUE
  e$startTime <- Sys.time()
  cb <- function(expr, val, ok, vis, data=e){
    e$expr <- expr
    e$val <- val
    e$ok <- ok
    e$vis <- vis
    # The result of f() will determine whether the callback
    # remains active
    return(f(e, ...))
  }
  addTaskCallback(cb, name = "swirllike")
  message("How much is 2+2?")
}

OK, so the swirllike function evokes the 2+2 question, but it also declares a new environment e with some objects the user needs not know. It then adds the swirllike task callback to the task callback list (or rather vector). This "task callback" holds the cb function which calls the f function - the f function will run with every input.

If you run this, make sure you see the swirllike task callback with:

> getTaskCallbackNames()
[1] "swirllike"

Now the f function is similar to my sketch in the question:

f <- function(e, ...){

  if (e$prompt) {
    if (difftime(Sys.time(), e$startTime, units = "mins") > 1) {
      timeout <- TRUE
      stop("Sorry, timeout.")
    }

    if(!is.null(.Last.value) && .Last.value == 4) {
      message("Correct! Let's continue with this function:")
      e$prompt <- FALSE
      while (!e$prompt) {
        #continue asking questions or something, but for this example:
        break
      }
    }
  }
  return(TRUE)
}

And don't forget to remove the swirllike task callback with:

removeTaskCallback("swirllike")

Upvotes: 1

Related Questions