diff --git a/NAMESPACE b/NAMESPACE index ac9ce731..3177d207 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -8,6 +8,7 @@ export(destroy_loop) export(exists_loop) export(global_loop) export(later) +export(later_recurring) export(loop_empty) export(next_op_secs) export(run_now) diff --git a/NEWS.md b/NEWS.md index 01c7e714..2ed72c57 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,5 +1,6 @@ ## later 1.1.0.9000 +* Added `later_recurring`, allowing a function to recur in its 'later' loop until an iteration limit (if set) or explicit cancellation. PR #133 ## later 1.1.0.1 diff --git a/R/is_false.R b/R/is_false.R new file mode 100644 index 00000000..ffe49aa9 --- /dev/null +++ b/R/is_false.R @@ -0,0 +1 @@ +is_false <- function(x) is.logical(x) && length(x) == 1L && !is.na(x) && !x diff --git a/R/later.R b/R/later.R index 91dcd32f..488f03da 100644 --- a/R/later.R +++ b/R/later.R @@ -70,7 +70,7 @@ create_loop <- function(parent = current_loop(), autorun = NULL) { # This is for backward compatibility, if `create_loop(autorun=FALSE)` is called. parent <- NULL } - if (identical(parent, FALSE)) { + if (is_false(parent)) { # This is for backward compatibility, if `create_loop(FALSE)` is called. # (Previously the first and only parameter was `autorun`.) parent <- NULL diff --git a/R/later_recurring.R b/R/later_recurring.R new file mode 100644 index 00000000..9ba846aa --- /dev/null +++ b/R/later_recurring.R @@ -0,0 +1,37 @@ +#' @describeIn later Schedules a recurring task +#' @details +#' +#' In `later_recurring`, if `func` returns an explicit `FALSE` then +#' this is interpreted as self-cancelling the loop. Anything else +#' returned (including multiple `FALSE`) is ignored. +#' +#' @param limit Number of times to repeat the function. If `Inf` (the +#' default) then no limit. +#' @examples +#' # Limit number of executions to 3 times +#' later_recurring(~ message("Hello from the past"), 1, limit = 3) +#' +#' # Stop recurring when the return value is `FALSE` +#' later_recurring(function() { +#' message("Flipping a coin to see if we run again...") +#' sample(c(TRUE, FALSE), size = 1L) +#' }, 0.25, limit = Inf) +#' @export +later_recurring <- function(func, delay, limit = Inf, loop = current_loop()) { + func <- rlang::as_function(func) + cancelled <- FALSE + if (is.na(limit) || limit < 1) + stop("'limit' must be a positive number") + func2 <- function() { + limit <<- limit - 1L + ret <- func() + if (is_false(ret)) cancelled <<- TRUE + if (!cancelled && limit > 0) + handle <<- later(func2, delay, loop) + } + handle <- later(func2, delay, loop) + invisible(function() { + cancelled <<- TRUE + handle() + }) +} diff --git a/man/later.Rd b/man/later.Rd index cc03b9b9..e43245e2 100644 --- a/man/later.Rd +++ b/man/later.Rd @@ -1,10 +1,13 @@ % Generated by roxygen2: do not edit by hand -% Please edit documentation in R/later.R +% Please edit documentation in R/later.R, R/later_recurring.R \name{later} \alias{later} +\alias{later_recurring} \title{Executes a function later} \usage{ later(func, delay = 0, loop = current_loop()) + +later_recurring(func, delay, limit = Inf, loop = current_loop()) } \arguments{ \item{func}{A function or formula (see \code{\link[rlang:as_function]{rlang::as_function()}}).} @@ -14,6 +17,9 @@ guarantee that the function will be executed at the desired time, but it should not execute earlier.} \item{loop}{A handle to an event loop. Defaults to the currently-active loop.} + +\item{limit}{Number of times to repeat the function. If \code{Inf} (the +default) then no limit.} } \value{ A function, which, if invoked, will cancel the callback. The @@ -31,7 +37,16 @@ at the requested time, only that at least that much time will elapse. The mechanism used by this package is inspired by Simon Urbanek's \href{https://github.com/s-u/background}{background} package and similar code in Rhttpd. + +In \code{later_recurring}, if \code{func} returns an explicit \code{FALSE} then +this is interpreted as self-cancelling the loop. Anything else +returned (including multiple \code{FALSE}) is ignored. } +\section{Functions}{ +\itemize{ +\item \code{later_recurring}: Schedules a recurring task +}} + \note{ To avoid bugs due to reentrancy, by default, scheduled operations only run when there is no other R code present on the execution stack; i.e., when R is @@ -53,4 +68,12 @@ later(function() { print(summary(cars)) }, 2) +# Limit number of executions to 3 times +later_recurring(~ message("Hello from the past"), 1, limit = 3) + +# Stop recurring when the return value is `FALSE` +later_recurring(function() { + message("Flipping a coin to see if we run again...") + sample(c(TRUE, FALSE), size = 1L) +}, 0.25, limit = Inf) } diff --git a/tests/testthat/test-recurring.R b/tests/testthat/test-recurring.R new file mode 100644 index 00000000..385d08e5 --- /dev/null +++ b/tests/testthat/test-recurring.R @@ -0,0 +1,27 @@ +context("test-recurring") + +test_that("Limited recurrence", { + # Repeat until the limit reached, stopped by limit + x <- 0 + later_recurring(function() { x <<- x + 1 }, delay = 0.5, limit = 2) + run_now(0.5) + expect_identical(x, 1) + run_now(0.5) + expect_identical(x, 2) + run_now(1) + expect_identical(x, 2) +}) + +test_that("Self-cancelling recurrence", { + # Repeat until the function returns FALSE, self-cancelling + x <- 0 + cancel <- later_recurring(function() { x <<- x + 1; (x < 2) }, delay = 0.5, limit = 4) + expect_identical(length(list_queue()), 1L) + run_now(0.5) + expect_identical(x, 1) + run_now(0.5) + expect_identical(x, 2) + run_now(1) + expect_identical(x, 2) + expect_identical(length(list_queue()), 0L) +})