From 1fe30a308f40fc7d30734782e9c0ffaf1fa517dc Mon Sep 17 00:00:00 2001 From: Bill Evans Date: Mon, 6 Jul 2020 08:55:01 -0700 Subject: [PATCH 1/5] add later_recurring --- NEWS.md | 1 + R/later_recurring.R | 22 ++++++++++++++++++++++ man/later.Rd | 14 +++++++++++++- 3 files changed, 36 insertions(+), 1 deletion(-) create mode 100644 R/later_recurring.R diff --git a/NEWS.md b/NEWS.md index 01c7e714..55b5dcbb 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. ## later 1.1.0.1 diff --git a/R/later_recurring.R b/R/later_recurring.R new file mode 100644 index 00000000..6f19394c --- /dev/null +++ b/R/later_recurring.R @@ -0,0 +1,22 @@ +#' @describeIn later Schedules a recurring task +#' @param limit Number of times to repeat the function. If `NA` (the default) +#' then no limit. +#' @examples +#' later_recurring(~cat("Hello from the past\n"), 3, limit = 2) +later_recurring <- function(func, delay, limit = NA, loop = current_loop()) { + func <- rlang::as_function(func) + cancelled <- FALSE + if (!is.na(limit) && limit < 1) + stop("'limit' must be 'NA' or a positive number") + func2 <- function() { + limit <<- limit - 1L + func() + if (!cancelled && (is.na(limit) || 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..80b72acf 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 = NA, 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{NA} (the default) +then no limit.} } \value{ A function, which, if invoked, will cancel the callback. The @@ -32,6 +38,11 @@ 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. } +\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 +64,5 @@ later(function() { print(summary(cars)) }, 2) +later_recurring(~cat("Hello from the past\n"), 3, limit = 2) } From d8771c6a282ca61b3e637040a187e0e35ba8d88d Mon Sep 17 00:00:00 2001 From: Bill Evans Date: Mon, 6 Jul 2020 09:10:01 -0700 Subject: [PATCH 2/5] export, add PR number --- NAMESPACE | 1 + NEWS.md | 2 +- R/later_recurring.R | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) 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 55b5dcbb..2ed72c57 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,6 +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. +* 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/later_recurring.R b/R/later_recurring.R index 6f19394c..afe31cfe 100644 --- a/R/later_recurring.R +++ b/R/later_recurring.R @@ -3,6 +3,7 @@ #' then no limit. #' @examples #' later_recurring(~cat("Hello from the past\n"), 3, limit = 2) +#' @export later_recurring <- function(func, delay, limit = NA, loop = current_loop()) { func <- rlang::as_function(func) cancelled <- FALSE From 4837a5d60d5e0ddb0d742d4a0e017731adb0b855 Mon Sep 17 00:00:00 2001 From: Bill Evans Date: Tue, 30 Nov 2021 09:40:28 -0500 Subject: [PATCH 3/5] add optional self-cancelling to recurring func - if the 'func()' returns a logical, then its return value is taken as a "continue" logical; that is, if 'func()' returns true, then continue, if false then stop (the recurrency is cancelled) --- R/later_recurring.R | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/R/later_recurring.R b/R/later_recurring.R index afe31cfe..a863178d 100644 --- a/R/later_recurring.R +++ b/R/later_recurring.R @@ -11,7 +11,8 @@ later_recurring <- function(func, delay, limit = NA, loop = current_loop()) { stop("'limit' must be 'NA' or a positive number") func2 <- function() { limit <<- limit - 1L - func() + ret <- func() + if (is.logical(ret) && !anyNA(ret)) cancelled <<- !ret[1] if (!cancelled && (is.na(limit) || limit > 0)) handle <<- later(func2, delay, loop) } From 9b361db571ce7f89f1a26ee580f36287f153dfcb Mon Sep 17 00:00:00 2001 From: Bill Evans Date: Wed, 1 Dec 2021 14:37:55 -0500 Subject: [PATCH 4/5] update later_recurring: default limit, doc, testing - change limit= default to Inf (NA no longer accepted) - add is_false (and one update to later.R) - update doc (details and example code) --- R/is_false.R | 1 + R/later.R | 2 +- R/later_recurring.R | 29 +++++++++++++++++++++-------- man/later.Rd | 19 +++++++++++++++---- tests/testthat/test-recurring.R | 27 +++++++++++++++++++++++++++ 5 files changed, 65 insertions(+), 13 deletions(-) create mode 100644 R/is_false.R create mode 100644 tests/testthat/test-recurring.R 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 index a863178d..a593b751 100644 --- a/R/later_recurring.R +++ b/R/later_recurring.R @@ -1,19 +1,32 @@ #' @describeIn later Schedules a recurring task -#' @param limit Number of times to repeat the function. If `NA` (the default) -#' then no limit. +#' @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 -#' later_recurring(~cat("Hello from the past\n"), 3, limit = 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) #' @export -later_recurring <- function(func, delay, limit = NA, loop = current_loop()) { +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 'NA' or a positive number") + if (is.na(limit) || limit < 1) + stop("'limit' must be a positive number") func2 <- function() { limit <<- limit - 1L ret <- func() - if (is.logical(ret) && !anyNA(ret)) cancelled <<- !ret[1] - if (!cancelled && (is.na(limit) || limit > 0)) + if (is_false(ret)) cancelled <<- !ret[1] + if (!cancelled && limit > 0) handle <<- later(func2, delay, loop) } handle <- later(func2, delay, loop) diff --git a/man/later.Rd b/man/later.Rd index 80b72acf..e43245e2 100644 --- a/man/later.Rd +++ b/man/later.Rd @@ -7,7 +7,7 @@ \usage{ later(func, delay = 0, loop = current_loop()) -later_recurring(func, delay, limit = NA, 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()}}).} @@ -18,8 +18,8 @@ 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{NA} (the default) -then no limit.} +\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 @@ -37,6 +37,10 @@ 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{ @@ -64,5 +68,12 @@ later(function() { print(summary(cars)) }, 2) -later_recurring(~cat("Hello from the past\n"), 3, limit = 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) +}) From ce85ed2e259aacb1fc805a1ec82b349e4ce692a5 Mon Sep 17 00:00:00 2001 From: Bill Evans Date: Wed, 1 Dec 2021 15:00:28 -0500 Subject: [PATCH 5/5] be more explicit when setting cancelled to true --- R/later_recurring.R | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/R/later_recurring.R b/R/later_recurring.R index a593b751..9ba846aa 100644 --- a/R/later_recurring.R +++ b/R/later_recurring.R @@ -25,7 +25,7 @@ later_recurring <- function(func, delay, limit = Inf, loop = current_loop()) { func2 <- function() { limit <<- limit - 1L ret <- func() - if (is_false(ret)) cancelled <<- !ret[1] + if (is_false(ret)) cancelled <<- TRUE if (!cancelled && limit > 0) handle <<- later(func2, delay, loop) }