diff --git a/NAMESPACE b/NAMESPACE
index 669ec08..a418ce0 100644
--- a/NAMESPACE
+++ b/NAMESPACE
@@ -27,6 +27,7 @@ export(evaluate)
export(export)
export(get_option)
export(make_logo)
+export(par_apply)
export(par_lapply)
export(par_sapply)
export(peek)
@@ -43,6 +44,7 @@ importFrom(parallel,clusterEvalQ)
importFrom(parallel,clusterExport)
importFrom(parallel,detectCores)
importFrom(parallel,makeCluster)
+importFrom(parallel,parApply)
importFrom(parallel,parLapply)
importFrom(parallel,parSapply)
importFrom(parallel,stopCluster)
diff --git a/NEWS.md b/NEWS.md
index 43dd81a..69d67eb 100644
--- a/NEWS.md
+++ b/NEWS.md
@@ -1,6 +1,12 @@
# Development
## Added
+- Update implementations of `Service$apply` operation for `Backend` classes to
+ validate the provided `margin` argument before running the parallel operation.
+- Add helper `Helper$check_array_margins` to validate the margins provided to
+ the `Service$apply` operation.
+- Add exception `Exception$array_margins_not_compatible` for using improper
+ margins in the `Service$apply` operation.
- Add exception `Exception$primitive_as_task_not_allowed` for trying to decorate
primitive functions with progress tracking in the `ProgressTrackingContext`
class.
@@ -8,8 +14,11 @@
- Add optional arguments to the `get_output` operation of `SyncBackend` for
consistency.
- Add more tests to improve coverage.
-- Add `par_lapply` function to the user `API`. The `par_lapply` function can be
- used to run tasks in parallel akin to `parallel::parLapply`.
+- Add implementation for `Service$lapply` and `Service$apply` operations for all
+ classes that implement the `Service` interface.
+- Add `par_lapply` and `par_apply` functions to the user `API`. These functions
+ can be used to run tasks in parallel akin to `parallel::parLapply` and
+ `parallel::parApply`, respectively.
- Add `UserApiConsumer` `R6` class that provides an opinionated wrapper around
the developer `API` of the `parabar` package. All parallel operations (e.g.,
`par_sapply` and `par_lapply`) follow more or less the same pattern. The
diff --git a/R/AsyncBackend.R b/R/AsyncBackend.R
index 71933a5..d72fded 100644
--- a/R/AsyncBackend.R
+++ b/R/AsyncBackend.R
@@ -240,6 +240,21 @@ AsyncBackend <- R6::R6Class("AsyncBackend",
}, args = list(x, fun, dots))
},
+ # Run tasks asynchronously via the cluster in the session.
+ .apply = function(x, margin, fun, ...) {
+ # Capture the `...`.
+ dots <- list(...)
+
+ # Perform the evaluation from the `R` session.
+ private$.cluster$call(function(x, margin, fun, dots) {
+ # Run the task.
+ output <- do.call(parallel::parApply, c(list(cluster, x, margin, fun), dots))
+
+ # Return to the session.
+ return(output)
+ }, args = list(x, margin, fun, dots))
+ },
+
# Clear the current output on the backend.
.clear_output = function() {
# Clear output.
@@ -480,6 +495,39 @@ AsyncBackend <- R6::R6Class("AsyncBackend",
private$.lapply(x, fun, ...)
},
+
+ #' @description
+ #' Run a task on the backend akin to [parallel::parApply()].
+ #'
+ #' @param x An array to pass to the `fun` function.
+ #'
+ #' @param margin A numeric vector indicating the dimensions of `x` the
+ #' `fun` function should be applied over. For example, for a matrix,
+ #' `margin = 1` indicates applying `fun` rows-wise, `margin = 2`
+ #' indicates applying `fun` columns-wise, and `margin = c(1, 2)`
+ #' indicates applying `fun` element-wise. Named dimensions are also
+ #' possible depending on `x`. See [parallel::parApply()] and
+ #' [base::apply()] for more details.
+ #'
+ #' @param fun A function to apply to `x` according to the `margin`.
+ #'
+ #' @param ... Additional arguments to pass to the `fun` function.
+ #'
+ #' @return
+ #' This method returns void. The output of the task execution must be
+ #' stored in the private field `.output` on the [`parabar::Backend`]
+ #' abstract class, and is accessible via the `get_output()` method.
+ apply = function(x, margin, fun, ...) {
+ # Throw if backend is busy.
+ private$.throw_if_backend_is_busy()
+
+ # Validate provided margins.
+ Helper$check_array_margins(margin, dim(x))
+
+ # Deploy the task asynchronously.
+ private$.apply(x, margin, fun, ...)
+ },
+
#' @description
#' Get the output of the task execution.
#'
diff --git a/R/Context.R b/R/Context.R
index 4b0687f..104666e 100644
--- a/R/Context.R
+++ b/R/Context.R
@@ -223,6 +223,32 @@ Context <- R6::R6Class("Context",
private$.backend$lapply(x = x, fun = fun, ...)
},
+ #' @description
+ #' Run a task on the backend akin to [parallel::parApply()].
+ #'
+ #' @param x An array to pass to the `fun` function.
+ #'
+ #' @param margin A numeric vector indicating the dimensions of `x` the
+ #' `fun` function should be applied over. For example, for a matrix,
+ #' `margin = 1` indicates applying `fun` rows-wise, `margin = 2`
+ #' indicates applying `fun` columns-wise, and `margin = c(1, 2)`
+ #' indicates applying `fun` element-wise. Named dimensions are also
+ #' possible depending on `x`. See [parallel::parApply()] and
+ #' [base::apply()] for more details.
+ #'
+ #' @param fun A function to apply to `x` according to the `margin`.
+ #'
+ #' @param ... Additional arguments to pass to the `fun` function.
+ #'
+ #' @return
+ #' This method returns void. The output of the task execution must be
+ #' stored in the private field `.output` on the [`parabar::Backend`]
+ #' abstract class, and is accessible via the `get_output()` method.
+ apply = function(x, margin, fun, ...) {
+ # Consume the backend API.
+ private$.backend$apply(x = x, margin = margin, fun = fun, ...)
+ },
+
#' @description
#' Get the output of the task execution.
#'
diff --git a/R/Exception.R b/R/Exception.R
index 1a1fc31..317de72 100644
--- a/R/Exception.R
+++ b/R/Exception.R
@@ -22,6 +22,7 @@
#' \item{\code{Exception$type_not_assignable()}}{Exception for when providing incorrect object types.}
#' \item{\code{Exception$unknown_package_option()}}{Exception for when requesting unknown package options.}
#' \item{\code{Exception$primitive_as_task_not_allowed()}}{Exception for when decorating primitive functions with progress tracking.}
+#' \item{\code{Exception$array_margins_not_compatible(actual, allowed)}}{Exception for using improper margins in the `Service$apply` operation.}
#' }
#'
#' @export
@@ -127,3 +128,19 @@ Exception$primitive_as_task_not_allowed <- function() {
stop(message, call. = FALSE)
}
+# Exception for providing incompatible margins in the `apply` operation.
+Exception$array_margins_not_compatible <- function(margins, dimensions) {
+ # Convert the margins to character.
+ margins <- paste(margins, collapse = ", ")
+
+ # Convert the dimensions to character.
+ dimensions <- paste(dimensions, collapse = ", ")
+
+ # Construct exception message.
+ message = paste0(
+ "Margins {", margins, "} not compatible with array dimensions {", dimensions, "}."
+ )
+
+ # Throw the error.
+ stop(message, call. = FALSE)
+}
diff --git a/R/Helper.R b/R/Helper.R
index e776993..5f4b441 100644
--- a/R/Helper.R
+++ b/R/Helper.R
@@ -13,6 +13,7 @@
#' \item{\code{Helper$get_option()}}{Get package option, or corresponding default value.}
#' \item{\code{Helper$set_option()}}{Set package option.}
#' \item{\code{Helper$check_object_type()}}{Check the type of a given object.}
+#' \item{\code{Helper$check_array_margins(margins, dimensions)}}{Helper to check array margins for the `Service$apply` operation.}
#' }
#'
#' @export
@@ -74,3 +75,21 @@ Helper$check_object_type <- function(object, expected_type) {
Exception$type_not_assignable(type, expected_type)
}
}
+
+# Helper for checking the array margins provided for the `apply` operation.
+Helper$check_array_margins <- function(margins, dimensions) {
+ # Conditions to ensure the margins are valid.
+ violations <- c(
+ # Ensure all margins are unique.
+ duplicated(margins),
+
+ # Ensure all margins are within the array dimensions.
+ margins > length(dimensions)
+ )
+
+ # If any violations are found.
+ if (any(violations)) {
+ # Throw an error.
+ Exception$array_margins_not_compatible(margins, dimensions)
+ }
+}
diff --git a/R/ProgressTrackingContext.R b/R/ProgressTrackingContext.R
index cb84c07..cdc000b 100644
--- a/R/ProgressTrackingContext.R
+++ b/R/ProgressTrackingContext.R
@@ -353,6 +353,42 @@ ProgressTrackingContext <- R6::R6Class("ProgressTrackingContext",
# Execute the task via the `lapply` backend operation.
private$.execute(operation = operation, fun = fun, total = length(x))
+ },
+
+ #' @description
+ #' Run a task on the backend akin to [parallel::parApply()].
+ #'
+ #' @param x An array to pass to the `fun` function.
+ #'
+ #' @param margin A numeric vector indicating the dimensions of `x` the
+ #' `fun` function should be applied over. For example, for a matrix,
+ #' `margin = 1` indicates applying `fun` rows-wise, `margin = 2`
+ #' indicates applying `fun` columns-wise, and `margin = c(1, 2)`
+ #' indicates applying `fun` element-wise. Named dimensions are also
+ #' possible depending on `x`. See [parallel::parApply()] and
+ #' [base::apply()] for more details.
+ #'
+ #' @param fun A function to apply to `x` according to the `margin`.
+ #'
+ #' @param ... Additional arguments to pass to the `fun` function.
+ #'
+ #' @return
+ #' This method returns void. The output of the task execution must be
+ #' stored in the private field `.output` on the [`parabar::Backend`]
+ #' abstract class, and is accessible via the `get_output()` method.
+ apply = function(x, margin, fun, ...) {
+ # Determine the number of task executions.
+ total <- prod(dim(x)[margin])
+
+ # Prepare the backend operation with early evaluated `...`.
+ operation <- bquote(
+ do.call(
+ super$apply, c(list(x = .(x), margin = .(margin), fun = fun), .(list(...)))
+ )
+ )
+
+ # Execute the task via the `lapply` backend operation.
+ private$.execute(operation = operation, fun = fun, total = total)
}
),
diff --git a/R/Service.R b/R/Service.R
index 1e180da..ba9da4e 100644
--- a/R/Service.R
+++ b/R/Service.R
@@ -132,6 +132,31 @@ Service <- R6::R6Class("Service",
Exception$method_not_implemented()
},
+ #' @description
+ #' Run a task on the backend akin to [parallel::parApply()].
+ #'
+ #' @param x An array to pass to the `fun` function.
+ #'
+ #' @param margin A numeric vector indicating the dimensions of `x` the
+ #' `fun` function should be applied over. For example, for a matrix,
+ #' `margin = 1` indicates applying `fun` rows-wise, `margin = 2`
+ #' indicates applying `fun` columns-wise, and `margin = c(1, 2)`
+ #' indicates applying `fun` element-wise. Named dimensions are also
+ #' possible depending on `x`. See [parallel::parApply()] and
+ #' [base::apply()] for more details.
+ #'
+ #' @param fun A function to apply to `x` according to the `margin`.
+ #'
+ #' @param ... Additional arguments to pass to the `fun` function.
+ #'
+ #' @return
+ #' This method returns void. The output of the task execution must be
+ #' stored in the private field `.output` on the [`parabar::Backend`]
+ #' abstract class, and is accessible via the `get_output()` method.
+ apply = function(x, margin, fun, ...) {
+ Exception$method_not_implemented()
+ },
+
#' @description
#' Get the output of the task execution.
#'
diff --git a/R/SyncBackend.R b/R/SyncBackend.R
index 7495632..7b12c04 100644
--- a/R/SyncBackend.R
+++ b/R/SyncBackend.R
@@ -163,6 +163,12 @@ SyncBackend <- R6::R6Class("SyncBackend",
parallel::parLapply(private$.cluster, X = x, fun = fun, ...)
},
+ # A wrapper around `parallel:parApply` to run tasks on the cluster.
+ .apply = function(x, margin, fun, ...) {
+ # Run the task and return the results.
+ parallel::parApply(private$.cluster, X = x, MARGIN = margin, FUN = fun, ...)
+ },
+
# Clear the current output on the backend.
.clear_output = function() {
# Clear output.
@@ -310,6 +316,35 @@ SyncBackend <- R6::R6Class("SyncBackend",
private$.output = private$.lapply(x, fun, ...)
},
+ #' @description
+ #' Run a task on the backend akin to [parallel::parApply()].
+ #'
+ #' @param x An array to pass to the `fun` function.
+ #'
+ #' @param margin A numeric vector indicating the dimensions of `x` the
+ #' `fun` function should be applied over. For example, for a matrix,
+ #' `margin = 1` indicates applying `fun` rows-wise, `margin = 2`
+ #' indicates applying `fun` columns-wise, and `margin = c(1, 2)`
+ #' indicates applying `fun` element-wise. Named dimensions are also
+ #' possible depending on `x`. See [parallel::parApply()] and
+ #' [base::apply()] for more details.
+ #'
+ #' @param fun A function to apply to `x` according to the `margin`.
+ #'
+ #' @param ... Additional arguments to pass to the `fun` function.
+ #'
+ #' @return
+ #' This method returns void. The output of the task execution must be
+ #' stored in the private field `.output` on the [`parabar::Backend`]
+ #' abstract class, and is accessible via the `get_output()` method.
+ apply = function(x, margin, fun, ...) {
+ # Validate provided margins.
+ Helper$check_array_margins(margin, dim(x))
+
+ # Deploy the task synchronously.
+ private$.output = private$.apply(x, margin, fun, ...)
+ },
+
#' @description
#' Get the output of the task execution.
#'
diff --git a/R/UserApiConsumer.R b/R/UserApiConsumer.R
index 39b86c5..b4ddc15 100644
--- a/R/UserApiConsumer.R
+++ b/R/UserApiConsumer.R
@@ -226,6 +226,48 @@ UserApiConsumer <- R6::R6Class("UserApiConsumer",
)
)
+ # Execute the `lapply` operation accordingly and return the results.
+ private$.execute(backend, parallel, sequential)
+ },
+
+ #' @description
+ #' Execute a task in parallel akin to [parallel::parApply()].
+ #'
+ #' @param backend An object of class [`parabar::Backend`] as returned by
+ #' the [parabar::start_backend()] function. It can also be `NULL` to run
+ #' the task sequentially via [base::apply()].
+ #'
+ #' @param x An array to pass to the `fun` function.
+ #'
+ #' @param margin A numeric vector indicating the dimensions of `x` the
+ #' `fun` function should be applied over. For example, for a matrix,
+ #' `margin = 1` indicates applying `fun` rows-wise, `margin = 2`
+ #' indicates applying `fun` columns-wise, and `margin = c(1, 2)`
+ #' indicates applying `fun` element-wise. Named dimensions are also
+ #' possible depending on `x`. See [parallel::parApply()] and
+ #' [base::apply()] for more details.
+ #'
+ #' @param fun A function to apply to `x` according to the `margin`.
+ #'
+ #' @return
+ #' The dimensions of the output vary according to the `margin` argument.
+ #' Consult the documentation of [base::apply()] for a detailed
+ #' explanation on how the output is structured.
+ apply = function(backend, x, margin, fun, ...) {
+ # Prepare the sequential operation.
+ sequential <- bquote(
+ do.call(
+ base::apply, c(list(X = .(x), MARGIN = .(margin), FUN = .(fun)), .(list(...)))
+ )
+ )
+
+ # Prepare the parallel operation.
+ parallel <- bquote(
+ do.call(
+ context$apply, c(list(x = .(x), margin = .(margin), fun = .(fun)), .(list(...)))
+ )
+ )
+
# Execute the `lapply` operation accordingly and return the results.
private$.execute(backend, parallel, sequential)
}
diff --git a/R/exports.r b/R/exports.r
index e8338d7..ac03162 100644
--- a/R/exports.r
+++ b/R/exports.r
@@ -171,6 +171,16 @@ par_lapply <- function(backend = NULL, x, fun, ...) {
# Create an user API consumer.
consumer <- UserApiConsumer$new()
- # Execute the task using the `sapply` parallel operation.
+ # Execute the task using the `lapply` parallel operation.
consumer$lapply(backend = backend, x = x, fun = fun, ...)
}
+
+#' @template par-apply
+#' @export
+par_apply <- function(backend = NULL, x, margin, fun, ...) {
+ # Create an user API consumer.
+ consumer <- UserApiConsumer$new()
+
+ # Execute the task using the `apply` parallel operation.
+ consumer$apply(backend = backend, x = x, margin = margin, fun = fun, ...)
+}
diff --git a/R/parabar-package.R b/R/parabar-package.R
index 21a4fb5..f673403 100644
--- a/R/parabar-package.R
+++ b/R/parabar-package.R
@@ -17,7 +17,7 @@
# Imports.
#' @importFrom parallel detectCores makeCluster stopCluster clusterExport
-#' @importFrom parallel clusterEvalQ parSapply parLapply clusterCall
+#' @importFrom parallel clusterEvalQ parSapply parLapply parApply clusterCall
#' @importFrom R6 R6Class
#' @importFrom progress progress_bar
#' @importFrom callr r_session
diff --git a/_pkgdown.yml b/_pkgdown.yml
index 1eddfcc..1aec39e 100644
--- a/_pkgdown.yml
+++ b/_pkgdown.yml
@@ -14,6 +14,7 @@ reference:
- evaluate
- par_sapply
- par_lapply
+ - par_apply
- configure_bar
- matches("get_|set_")
- subtitle: Developer Classes
diff --git a/man-roxygen/clear.R b/man-roxygen/clear.R
index 61d6fa3..3077528 100644
--- a/man-roxygen/clear.R
+++ b/man-roxygen/clear.R
@@ -23,4 +23,5 @@
#' @seealso
#' [parabar::start_backend()], [parabar::peek()], [parabar::export()],
#' [parabar::evaluate()], [parabar::configure_bar()], [parabar::par_sapply()],
-#' [parabar::par_lapply()], [parabar::stop_backend()], and [`parabar::Service`].
+#' [parabar::par_lapply()], [parabar::par_apply()], [parabar::stop_backend()],
+#' and [`parabar::Service`].
diff --git a/man-roxygen/evaluate.R b/man-roxygen/evaluate.R
index 88c6e0d..ef00f47 100644
--- a/man-roxygen/evaluate.R
+++ b/man-roxygen/evaluate.R
@@ -8,7 +8,7 @@
#' @param backend An object of class [`parabar::Backend`] as returned by the
#' [parabar::start_backend()] function.
#'
- #' @param expression An unquoted expression to evaluate on the backend.
+#' @param expression An unquoted expression to evaluate on the backend.
#'
#' @details
#' This function is a convenience wrapper around the lower-lever API of
@@ -26,4 +26,5 @@
#' @seealso
#' [parabar::start_backend()], [parabar::peek()], [parabar::export()],
#' [parabar::clear()], [parabar::configure_bar()], [parabar::par_sapply()],
-#' [parabar::par_lapply()], [parabar::stop_backend()], and [`parabar::Service`].
+#' [parabar::par_lapply()], [parabar::par_apply()], [parabar::stop_backend()],
+#' and [`parabar::Service`].
diff --git a/man-roxygen/export.R b/man-roxygen/export.R
index e751c45..8fd01ad 100644
--- a/man-roxygen/export.R
+++ b/man-roxygen/export.R
@@ -29,4 +29,5 @@
#' @seealso
#' [parabar::start_backend()], [parabar::peek()], [parabar::evaluate()],
#' [parabar::clear()], [parabar::configure_bar()], [parabar::par_sapply()],
-#' [parabar::par_lapply()], [parabar::stop_backend()], and [`parabar::Service`].
+#' [parabar::par_lapply()], [parabar::par_apply()], [parabar::stop_backend()],
+#' and [`parabar::Service`].
diff --git a/man-roxygen/par-apply.R b/man-roxygen/par-apply.R
new file mode 100644
index 0000000..805f701
--- /dev/null
+++ b/man-roxygen/par-apply.R
@@ -0,0 +1,108 @@
+#' @title
+#' Run a Task in Parallel
+#'
+#' @description
+#' This function can be used to run a task in parallel. The task is executed in
+#' parallel on the specified backend, similar to [parallel::parApply()]. If
+#' `backend = NULL`, the task is executed sequentially using [base::apply()].
+#' See the **Details** section for more information on how this function works.
+#'
+#' @param backend An object of class [`parabar::Backend`] as returned by the
+#' [parabar::start_backend()] function. It can also be `NULL` to run the task
+#' sequentially via [base::apply()]. The default value is `NULL`.
+#'
+#' @param x An array to pass to the `fun` function.
+#'
+#' @param margin A numeric vector indicating the dimensions of `x` the
+#' `fun` function should be applied over. For example, for a matrix,
+#' `margin = 1` indicates applying `fun` rows-wise, `margin = 2`
+#' indicates applying `fun` columns-wise, and `margin = c(1, 2)`
+#' indicates applying `fun` element-wise. Named dimensions are also
+#' possible depending on `x`. See [parallel::parApply()] and
+#' [base::apply()] for more details.
+#'
+#' @param fun A function to apply to `x` according to the `margin`.
+#'
+#' @param ... Additional arguments to pass to the `fun` function.
+#'
+#' @details
+#' This function uses the [`parabar::UserApiConsumer`] class that acts like an
+#' interface for the developer API of the [`parabar::parabar`] package.
+#'
+#' @return
+#' The dimensions of the output vary according to the `margin` argument. Consult
+#' the documentation of [base::apply()] for a detailed explanation on how the
+#' output is structured.
+#'
+#' @examples
+#' \donttest{
+#'
+#' # Define a simple task.
+#' task <- function(x) {
+#' # Perform computations.
+#' Sys.sleep(0.01)
+#'
+#' # Return the result.
+#' mean(x)
+#' }
+#'
+#' # Define a matrix for the task.
+#' x <- matrix(rnorm(100^2, mean = 10, sd = 0.5), nrow = 100, ncol = 100)
+#'
+#' # Start an asynchronous backend.
+#' backend <- start_backend(cores = 2, cluster_type = "psock", backend_type = "async")
+#'
+#' # Run a task in parallel over the rows of `x`.
+#' results <- par_apply(backend, x = x, margin = 1, fun = task)
+#'
+#' # Run a task in parallel over the columns of `x`.
+#' results <- par_apply(backend, x = x, margin = 2, fun = task)
+#'
+#' # The task can also be run over all elements of `x` using `margin = c(1, 2)`.
+#' # Improper dimensions will throw an error.
+#' try(par_apply(backend, x = x, margin = c(1, 2, 3), fun = task))
+#'
+#' # Disable progress tracking.
+#' set_option("progress_track", FALSE)
+#'
+#' # Run a task in parallel.
+#' results <- par_apply(backend, x = x, margin = 1, fun = task)
+#'
+#' # Enable progress tracking.
+#' set_option("progress_track", TRUE)
+#'
+#' # Change the progress bar options.
+#' configure_bar(type = "modern", format = "[:bar] :percent")
+#'
+#' # Run a task in parallel.
+#' results <- par_apply(backend, x = x, margin = 1, fun = task)
+#'
+#' # Stop the backend.
+#' stop_backend(backend)
+#'
+#' # Start a synchronous backend.
+#' backend <- start_backend(cores = 2, cluster_type = "psock", backend_type = "sync")
+#'
+#' # Run a task in parallel.
+#' results <- par_apply(backend, x = x, margin = 1, fun = task)
+#'
+#' # Disable progress tracking to remove the warning that progress is not supported.
+#' set_option("progress_track", FALSE)
+#'
+#' # Run a task in parallel.
+#' results <- par_apply(backend, x = x, margin = 1, fun = task)
+#'
+#' # Stop the backend.
+#' stop_backend(backend)
+#'
+#' # Run the task using the `base::lapply` (i.e., non-parallel).
+#' results <- par_apply(NULL, x = x, margin = 1, fun = task)
+#'
+#' }
+#'
+#' @seealso
+#' [parabar::start_backend()], [parabar::peek()], [parabar::export()],
+#' [parabar::evaluate()], [parabar::clear()], [parabar::configure_bar()],
+#' [parabar::par_sapply()], [parabar::par_lapply()], [parabar::stop_backend()],
+#' [parabar::set_option()], [parabar::get_option()], [`parabar::Options`],
+#' [`parabar::UserApiConsumer`], and [`parabar::Service`].
diff --git a/man-roxygen/par-lapply.R b/man-roxygen/par-lapply.R
index b103d14..e4a9116 100644
--- a/man-roxygen/par-lapply.R
+++ b/man-roxygen/par-lapply.R
@@ -84,6 +84,6 @@
#' @seealso
#' [parabar::start_backend()], [parabar::peek()], [parabar::export()],
#' [parabar::evaluate()], [parabar::clear()], [parabar::configure_bar()],
-#' [parabar::par_sapply()], [parabar::stop_backend()], [parabar::set_option()],
-#' [parabar::get_option()], [`parabar::Options`], [`parabar::UserApiConsumer`],
-#' and [`parabar::Service`].
+#' [parabar::par_sapply()], [parabar::par_apply()], [parabar::stop_backend()],
+#' [parabar::set_option()], [parabar::get_option()], [`parabar::Options`],
+#' [`parabar::UserApiConsumer`], and [`parabar::Service`].
diff --git a/man-roxygen/par-sapply.R b/man-roxygen/par-sapply.R
index 9361a55..8463d0b 100644
--- a/man-roxygen/par-sapply.R
+++ b/man-roxygen/par-sapply.R
@@ -84,6 +84,6 @@
#' @seealso
#' [parabar::start_backend()], [parabar::peek()], [parabar::export()],
#' [parabar::evaluate()], [parabar::clear()], [parabar::configure_bar()],
-#' [parabar::par_lapply()], [parabar::stop_backend()], [parabar::set_option()],
-#' [parabar::get_option()], [`parabar::Options`], [`parabar::UserApiConsumer`],
-#' and [`parabar::Service`].
+#' [parabar::par_lapply()], [parabar::par_apply()], [parabar::stop_backend()],
+#' [parabar::set_option()], [parabar::get_option()], [`parabar::Options`],
+#' [`parabar::UserApiConsumer`], and [`parabar::Service`].
diff --git a/man-roxygen/parabar.R b/man-roxygen/parabar.R
index faef123..b5b6f98 100644
--- a/man-roxygen/parabar.R
+++ b/man-roxygen/parabar.R
@@ -23,6 +23,10 @@
#' [base::lapply()] function when no backend is provided. However, when a
#' backend is provided, the function will execute a task in parallel on the
#' backend, similar to the built-in function [parallel::parLapply()].
+#' - [parabar::par_apply()]: is a drop-in replacement for the built-in
+#' [base::apply()] function when no backend is provided. However, when a
+#' backend is provided, the function will execute a task in parallel on the
+#' backend, similar to the built-in function [parallel::parApply()].
#' - [parabar::clear()]: removes all variables available on a backend.
#' - [parabar::peek()]: returns the names of all variables available on a
#' backend.
@@ -60,8 +64,8 @@
#' [`start()`][parabar::Service], [`stop()`][parabar::Service],
#' [`clear()`][parabar::Service], [`peek()`][parabar::Service],
#' [`export()`][parabar::Service], [`evaluate()`][parabar::Service],
-#' [`sapply()`][parabar::Service], [`lapply()`][parabar::Service], and
-#' [`get_output()`][parabar::Service].
+#' [`sapply()`][parabar::Service], [`lapply()`][parabar::Service],
+#' [`apply()`][parabar::Service], and [`get_output()`][parabar::Service].
#'
#' Check out the documentation for [`parabar::Service`] for more information on
#' each method.
@@ -91,9 +95,12 @@
#' - [`parabar::BackendFactory`]: factory for creating backend objects.
#' - [`parabar::Context`]: default context for executing backend operations.
#' - [`parabar::ProgressTrackingContext`]: context for decorating the
-#' [`sapply()`][parabar::Service] and [`lapply()`][parabar::Service]
+#' [`sapply()`][parabar::Service], [`lapply()`][parabar::Service], and
+#' [`apply()`][parabar::Service]
#' operations to track and display the execution progress.
#' - [`parabar::ContextFactory`]: factory for creating context objects.
+#' - [`parabar::UserApiConsumer`]: opinionated wrapper around the other
+#' [`R6::R6`] classes used in by the exported functions for the users.
#'
#' @section Progress Bars:
#' [`parabar::parabar`] also exposes several classes for creating and updating
diff --git a/man-roxygen/peek.R b/man-roxygen/peek.R
index 88331f9..b47877a 100644
--- a/man-roxygen/peek.R
+++ b/man-roxygen/peek.R
@@ -25,4 +25,5 @@
#' @seealso
#' [parabar::start_backend()], [parabar::export()], [parabar::evaluate()],
#' [parabar::clear()], [parabar::configure_bar()], [parabar::par_sapply()],
-#' [parabar::par_lapply()], [parabar::stop_backend()], and [`parabar::Service`].
+#' [parabar::par_lapply()], [parabar::par_apply()], [parabar::stop_backend()],
+#' and [`parabar::Service`].
diff --git a/man-roxygen/start-backend.R b/man-roxygen/start-backend.R
index abcbbc8..1d751b0 100644
--- a/man-roxygen/start-backend.R
+++ b/man-roxygen/start-backend.R
@@ -120,4 +120,5 @@
#' @seealso
#' [parabar::peek()], [parabar::export()], [parabar::evaluate()],
#' [parabar::clear()], [parabar::configure_bar()], [parabar::par_sapply()],
-#' [parabar::par_lapply()], [parabar::stop_backend()], and [`parabar::Service`].
+#' [parabar::par_lapply()], [parabar::par_apply()], [parabar::stop_backend()],
+#' and [`parabar::Service`].
diff --git a/man-roxygen/stop-backend.R b/man-roxygen/stop-backend.R
index 20563ee..c2daede 100644
--- a/man-roxygen/stop-backend.R
+++ b/man-roxygen/stop-backend.R
@@ -26,4 +26,5 @@
#' @seealso
#' [parabar::start_backend()], [parabar::peek()], [parabar::export()],
#' [parabar::evaluate()], [parabar::clear()], [parabar::configure_bar()],
-#' [parabar::par_sapply()], [parabar::par_lapply()], and [`parabar::Service`].
+#' [parabar::par_sapply()], [parabar::par_apply()], [parabar::par_lapply()], and
+#' [`parabar::Service`].
diff --git a/man/AsyncBackend.Rd b/man/AsyncBackend.Rd
index 1f0311a..1506def 100644
--- a/man/AsyncBackend.Rd
+++ b/man/AsyncBackend.Rd
@@ -125,6 +125,7 @@ has been fetched, the backend is free to deploy another task.
\item \href{#method-AsyncBackend-evaluate}{\code{AsyncBackend$evaluate()}}
\item \href{#method-AsyncBackend-sapply}{\code{AsyncBackend$sapply()}}
\item \href{#method-AsyncBackend-lapply}{\code{AsyncBackend$lapply()}}
+\item \href{#method-AsyncBackend-apply}{\code{AsyncBackend$apply()}}
\item \href{#method-AsyncBackend-get_output}{\code{AsyncBackend$get_output()}}
\item \href{#method-AsyncBackend-clone}{\code{AsyncBackend$clone()}}
}
@@ -308,6 +309,40 @@ Run a task on the backend akin to \code{\link[parallel:clusterApply]{parallel::p
\item{\code{fun}}{A function to apply to each element of \code{x}.}
+\item{\code{...}}{Additional arguments to pass to the \code{fun} function.}
+}
+\if{html}{\out{}}
+}
+\subsection{Returns}{
+This method returns void. The output of the task execution must be
+stored in the private field \code{.output} on the \code{\link{Backend}}
+abstract class, and is accessible via the \code{get_output()} method.
+}
+}
+\if{html}{\out{
}}
+\if{html}{\out{}}
+\if{latex}{\out{\hypertarget{method-AsyncBackend-apply}{}}}
+\subsection{Method \code{apply()}}{
+Run a task on the backend akin to \code{\link[parallel:clusterApply]{parallel::parApply()}}.
+\subsection{Usage}{
+\if{html}{\out{}}\preformatted{AsyncBackend$apply(x, margin, fun, ...)}\if{html}{\out{
}}
+}
+
+\subsection{Arguments}{
+\if{html}{\out{}}
+\describe{
+\item{\code{x}}{An array to pass to the \code{fun} function.}
+
+\item{\code{margin}}{A numeric vector indicating the dimensions of \code{x} the
+\code{fun} function should be applied over. For example, for a matrix,
+\code{margin = 1} indicates applying \code{fun} rows-wise, \code{margin = 2}
+indicates applying \code{fun} columns-wise, and \code{margin = c(1, 2)}
+indicates applying \code{fun} element-wise. Named dimensions are also
+possible depending on \code{x}. See \code{\link[parallel:clusterApply]{parallel::parApply()}} and
+\code{\link[base:apply]{base::apply()}} for more details.}
+
+\item{\code{fun}}{A function to apply to \code{x} according to the \code{margin}.}
+
\item{\code{...}}{Additional arguments to pass to the \code{fun} function.}
}
\if{html}{\out{
}}
diff --git a/man/Backend.Rd b/man/Backend.Rd
index 22d4fe2..a7e83d3 100644
--- a/man/Backend.Rd
+++ b/man/Backend.Rd
@@ -48,6 +48,7 @@ implementation has an active cluster.}
\if{html}{\out{
Inherited methods
+parabar::Service$apply()
parabar::Service$clear()
parabar::Service$evaluate()
parabar::Service$export()
diff --git a/man/Context.Rd b/man/Context.Rd
index 9d5945b..6b015a1 100644
--- a/man/Context.Rd
+++ b/man/Context.Rd
@@ -102,6 +102,7 @@ context.}
\item \href{#method-Context-evaluate}{\code{Context$evaluate()}}
\item \href{#method-Context-sapply}{\code{Context$sapply()}}
\item \href{#method-Context-lapply}{\code{Context$lapply()}}
+\item \href{#method-Context-apply}{\code{Context$apply()}}
\item \href{#method-Context-get_output}{\code{Context$get_output()}}
\item \href{#method-Context-clone}{\code{Context$clone()}}
}
@@ -277,6 +278,40 @@ Run a task on the backend akin to \code{\link[parallel:clusterApply]{parallel::p
\item{\code{fun}}{A function to apply to each element of \code{x}.}
+\item{\code{...}}{Additional arguments to pass to the \code{fun} function.}
+}
+\if{html}{\out{}}
+}
+\subsection{Returns}{
+This method returns void. The output of the task execution must be
+stored in the private field \code{.output} on the \code{\link{Backend}}
+abstract class, and is accessible via the \code{get_output()} method.
+}
+}
+\if{html}{\out{
}}
+\if{html}{\out{}}
+\if{latex}{\out{\hypertarget{method-Context-apply}{}}}
+\subsection{Method \code{apply()}}{
+Run a task on the backend akin to \code{\link[parallel:clusterApply]{parallel::parApply()}}.
+\subsection{Usage}{
+\if{html}{\out{}}\preformatted{Context$apply(x, margin, fun, ...)}\if{html}{\out{
}}
+}
+
+\subsection{Arguments}{
+\if{html}{\out{}}
+\describe{
+\item{\code{x}}{An array to pass to the \code{fun} function.}
+
+\item{\code{margin}}{A numeric vector indicating the dimensions of \code{x} the
+\code{fun} function should be applied over. For example, for a matrix,
+\code{margin = 1} indicates applying \code{fun} rows-wise, \code{margin = 2}
+indicates applying \code{fun} columns-wise, and \code{margin = c(1, 2)}
+indicates applying \code{fun} element-wise. Named dimensions are also
+possible depending on \code{x}. See \code{\link[parallel:clusterApply]{parallel::parApply()}} and
+\code{\link[base:apply]{base::apply()}} for more details.}
+
+\item{\code{fun}}{A function to apply to \code{x} according to the \code{margin}.}
+
\item{\code{...}}{Additional arguments to pass to the \code{fun} function.}
}
\if{html}{\out{
}}
diff --git a/man/Exception.Rd b/man/Exception.Rd
index 6d19d39..85cf44f 100644
--- a/man/Exception.Rd
+++ b/man/Exception.Rd
@@ -18,6 +18,7 @@
\item{\code{Exception$type_not_assignable()}}{Exception for when providing incorrect object types.}
\item{\code{Exception$unknown_package_option()}}{Exception for when requesting unknown package options.}
\item{\code{Exception$primitive_as_task_not_allowed()}}{Exception for when decorating primitive functions with progress tracking.}
+\item{\code{Exception$array_margins_not_compatible(actual, allowed)}}{Exception for using improper margins in the \code{Service$apply} operation.}
}
}
\description{
diff --git a/man/Helper.Rd b/man/Helper.Rd
index 266bb65..12320e6 100644
--- a/man/Helper.Rd
+++ b/man/Helper.Rd
@@ -10,6 +10,7 @@
\item{\code{Helper$get_option()}}{Get package option, or corresponding default value.}
\item{\code{Helper$set_option()}}{Set package option.}
\item{\code{Helper$check_object_type()}}{Check the type of a given object.}
+\item{\code{Helper$check_array_margins(margins, dimensions)}}{Helper to check array margins for the \code{Service$apply} operation.}
}
}
\description{
diff --git a/man/ProgressTrackingContext.Rd b/man/ProgressTrackingContext.Rd
index 43710b5..b9e3a89 100644
--- a/man/ProgressTrackingContext.Rd
+++ b/man/ProgressTrackingContext.Rd
@@ -128,6 +128,7 @@ context$stop()
\item \href{#method-ProgressTrackingContext-configure_bar}{\code{ProgressTrackingContext$configure_bar()}}
\item \href{#method-ProgressTrackingContext-sapply}{\code{ProgressTrackingContext$sapply()}}
\item \href{#method-ProgressTrackingContext-lapply}{\code{ProgressTrackingContext$lapply()}}
+\item \href{#method-ProgressTrackingContext-apply}{\code{ProgressTrackingContext$apply()}}
\item \href{#method-ProgressTrackingContext-clone}{\code{ProgressTrackingContext$clone()}}
}
}
@@ -248,6 +249,40 @@ progress bar.
\item{\code{fun}}{A function to apply to each element of \code{x}.}
+\item{\code{...}}{Additional arguments to pass to the \code{fun} function.}
+}
+\if{html}{\out{}}
+}
+\subsection{Returns}{
+This method returns void. The output of the task execution must be
+stored in the private field \code{.output} on the \code{\link{Backend}}
+abstract class, and is accessible via the \code{get_output()} method.
+}
+}
+\if{html}{\out{
}}
+\if{html}{\out{}}
+\if{latex}{\out{\hypertarget{method-ProgressTrackingContext-apply}{}}}
+\subsection{Method \code{apply()}}{
+Run a task on the backend akin to \code{\link[parallel:clusterApply]{parallel::parApply()}}.
+\subsection{Usage}{
+\if{html}{\out{}}\preformatted{ProgressTrackingContext$apply(x, margin, fun, ...)}\if{html}{\out{
}}
+}
+
+\subsection{Arguments}{
+\if{html}{\out{}}
+\describe{
+\item{\code{x}}{An array to pass to the \code{fun} function.}
+
+\item{\code{margin}}{A numeric vector indicating the dimensions of \code{x} the
+\code{fun} function should be applied over. For example, for a matrix,
+\code{margin = 1} indicates applying \code{fun} rows-wise, \code{margin = 2}
+indicates applying \code{fun} columns-wise, and \code{margin = c(1, 2)}
+indicates applying \code{fun} element-wise. Named dimensions are also
+possible depending on \code{x}. See \code{\link[parallel:clusterApply]{parallel::parApply()}} and
+\code{\link[base:apply]{base::apply()}} for more details.}
+
+\item{\code{fun}}{A function to apply to \code{x} according to the \code{margin}.}
+
\item{\code{...}}{Additional arguments to pass to the \code{fun} function.}
}
\if{html}{\out{
}}
diff --git a/man/Service.Rd b/man/Service.Rd
index 2160b6b..baa441a 100644
--- a/man/Service.Rd
+++ b/man/Service.Rd
@@ -24,6 +24,7 @@ and \code{\link{Context}}.
\item \href{#method-Service-evaluate}{\code{Service$evaluate()}}
\item \href{#method-Service-sapply}{\code{Service$sapply()}}
\item \href{#method-Service-lapply}{\code{Service$lapply()}}
+\item \href{#method-Service-apply}{\code{Service$apply()}}
\item \href{#method-Service-get_output}{\code{Service$get_output()}}
\item \href{#method-Service-clone}{\code{Service$clone()}}
}
@@ -198,6 +199,40 @@ Run a task on the backend akin to \code{\link[parallel:clusterApply]{parallel::p
\item{\code{fun}}{A function to apply to each element of \code{x}.}
+\item{\code{...}}{Additional arguments to pass to the \code{fun} function.}
+}
+\if{html}{\out{}}
+}
+\subsection{Returns}{
+This method returns void. The output of the task execution must be
+stored in the private field \code{.output} on the \code{\link{Backend}}
+abstract class, and is accessible via the \code{get_output()} method.
+}
+}
+\if{html}{\out{
}}
+\if{html}{\out{}}
+\if{latex}{\out{\hypertarget{method-Service-apply}{}}}
+\subsection{Method \code{apply()}}{
+Run a task on the backend akin to \code{\link[parallel:clusterApply]{parallel::parApply()}}.
+\subsection{Usage}{
+\if{html}{\out{}}\preformatted{Service$apply(x, margin, fun, ...)}\if{html}{\out{
}}
+}
+
+\subsection{Arguments}{
+\if{html}{\out{}}
+\describe{
+\item{\code{x}}{An array to pass to the \code{fun} function.}
+
+\item{\code{margin}}{A numeric vector indicating the dimensions of \code{x} the
+\code{fun} function should be applied over. For example, for a matrix,
+\code{margin = 1} indicates applying \code{fun} rows-wise, \code{margin = 2}
+indicates applying \code{fun} columns-wise, and \code{margin = c(1, 2)}
+indicates applying \code{fun} element-wise. Named dimensions are also
+possible depending on \code{x}. See \code{\link[parallel:clusterApply]{parallel::parApply()}} and
+\code{\link[base:apply]{base::apply()}} for more details.}
+
+\item{\code{fun}}{A function to apply to \code{x} according to the \code{margin}.}
+
\item{\code{...}}{Additional arguments to pass to the \code{fun} function.}
}
\if{html}{\out{
}}
diff --git a/man/SyncBackend.Rd b/man/SyncBackend.Rd
index 73b2a16..d0084a7 100644
--- a/man/SyncBackend.Rd
+++ b/man/SyncBackend.Rd
@@ -94,6 +94,7 @@ backend$active
\item \href{#method-SyncBackend-evaluate}{\code{SyncBackend$evaluate()}}
\item \href{#method-SyncBackend-sapply}{\code{SyncBackend$sapply()}}
\item \href{#method-SyncBackend-lapply}{\code{SyncBackend$lapply()}}
+\item \href{#method-SyncBackend-apply}{\code{SyncBackend$apply()}}
\item \href{#method-SyncBackend-get_output}{\code{SyncBackend$get_output()}}
\item \href{#method-SyncBackend-clone}{\code{SyncBackend$clone()}}
}
@@ -277,6 +278,40 @@ Run a task on the backend akin to \code{\link[parallel:clusterApply]{parallel::p
\item{\code{fun}}{A function to apply to each element of \code{x}.}
+\item{\code{...}}{Additional arguments to pass to the \code{fun} function.}
+}
+\if{html}{\out{}}
+}
+\subsection{Returns}{
+This method returns void. The output of the task execution must be
+stored in the private field \code{.output} on the \code{\link{Backend}}
+abstract class, and is accessible via the \code{get_output()} method.
+}
+}
+\if{html}{\out{
}}
+\if{html}{\out{}}
+\if{latex}{\out{\hypertarget{method-SyncBackend-apply}{}}}
+\subsection{Method \code{apply()}}{
+Run a task on the backend akin to \code{\link[parallel:clusterApply]{parallel::parApply()}}.
+\subsection{Usage}{
+\if{html}{\out{}}\preformatted{SyncBackend$apply(x, margin, fun, ...)}\if{html}{\out{
}}
+}
+
+\subsection{Arguments}{
+\if{html}{\out{}}
+\describe{
+\item{\code{x}}{An array to pass to the \code{fun} function.}
+
+\item{\code{margin}}{A numeric vector indicating the dimensions of \code{x} the
+\code{fun} function should be applied over. For example, for a matrix,
+\code{margin = 1} indicates applying \code{fun} rows-wise, \code{margin = 2}
+indicates applying \code{fun} columns-wise, and \code{margin = c(1, 2)}
+indicates applying \code{fun} element-wise. Named dimensions are also
+possible depending on \code{x}. See \code{\link[parallel:clusterApply]{parallel::parApply()}} and
+\code{\link[base:apply]{base::apply()}} for more details.}
+
+\item{\code{fun}}{A function to apply to \code{x} according to the \code{margin}.}
+
\item{\code{...}}{Additional arguments to pass to the \code{fun} function.}
}
\if{html}{\out{
}}
diff --git a/man/UserApiConsumer.Rd b/man/UserApiConsumer.Rd
index 808780d..59c19a8 100644
--- a/man/UserApiConsumer.Rd
+++ b/man/UserApiConsumer.Rd
@@ -75,6 +75,7 @@ stop_backend(backend)
\itemize{
\item \href{#method-UserApiConsumer-sapply}{\code{UserApiConsumer$sapply()}}
\item \href{#method-UserApiConsumer-lapply}{\code{UserApiConsumer$lapply()}}
+\item \href{#method-UserApiConsumer-apply}{\code{UserApiConsumer$apply()}}
\item \href{#method-UserApiConsumer-clone}{\code{UserApiConsumer$clone()}}
}
}
@@ -137,6 +138,44 @@ The output format resembles that of \code{\link[base:lapply]{base::lapply()}}.
}
}
\if{html}{\out{
}}
+\if{html}{\out{}}
+\if{latex}{\out{\hypertarget{method-UserApiConsumer-apply}{}}}
+\subsection{Method \code{apply()}}{
+Execute a task in parallel akin to \code{\link[parallel:clusterApply]{parallel::parApply()}}.
+\subsection{Usage}{
+\if{html}{\out{}}\preformatted{UserApiConsumer$apply(backend, x, margin, fun, ...)}\if{html}{\out{
}}
+}
+
+\subsection{Arguments}{
+\if{html}{\out{}}
+\describe{
+\item{\code{backend}}{An object of class \code{\link{Backend}} as returned by
+the \code{\link[=start_backend]{start_backend()}} function. It can also be \code{NULL} to run
+the task sequentially via \code{\link[base:apply]{base::apply()}}.}
+
+\item{\code{x}}{An array to pass to the \code{fun} function.}
+
+\item{\code{margin}}{A numeric vector indicating the dimensions of \code{x} the
+\code{fun} function should be applied over. For example, for a matrix,
+\code{margin = 1} indicates applying \code{fun} rows-wise, \code{margin = 2}
+indicates applying \code{fun} columns-wise, and \code{margin = c(1, 2)}
+indicates applying \code{fun} element-wise. Named dimensions are also
+possible depending on \code{x}. See \code{\link[parallel:clusterApply]{parallel::parApply()}} and
+\code{\link[base:apply]{base::apply()}} for more details.}
+
+\item{\code{fun}}{A function to apply to \code{x} according to the \code{margin}.}
+
+\item{\code{...}}{Additional arguments to pass to the \code{fun} function.}
+}
+\if{html}{\out{
}}
+}
+\subsection{Returns}{
+The dimensions of the output vary according to the \code{margin} argument.
+Consult the documentation of \code{\link[base:apply]{base::apply()}} for a detailed
+explanation on how the output is structured.
+}
+}
+\if{html}{\out{
}}
\if{html}{\out{}}
\if{latex}{\out{\hypertarget{method-UserApiConsumer-clone}{}}}
\subsection{Method \code{clone()}}{
diff --git a/man/clear.Rd b/man/clear.Rd
index 115b4a2..b4e0e3c 100644
--- a/man/clear.Rd
+++ b/man/clear.Rd
@@ -84,5 +84,6 @@ backend$active
\seealso{
\code{\link[=start_backend]{start_backend()}}, \code{\link[=peek]{peek()}}, \code{\link[=export]{export()}},
\code{\link[=evaluate]{evaluate()}}, \code{\link[=configure_bar]{configure_bar()}}, \code{\link[=par_sapply]{par_sapply()}},
-\code{\link[=par_lapply]{par_lapply()}}, \code{\link[=stop_backend]{stop_backend()}}, and \code{\link{Service}}.
+\code{\link[=par_lapply]{par_lapply()}}, \code{\link[=par_apply]{par_apply()}}, \code{\link[=stop_backend]{stop_backend()}},
+and \code{\link{Service}}.
}
diff --git a/man/evaluate.Rd b/man/evaluate.Rd
index 14e643d..7474a77 100644
--- a/man/evaluate.Rd
+++ b/man/evaluate.Rd
@@ -87,5 +87,6 @@ backend$active
\seealso{
\code{\link[=start_backend]{start_backend()}}, \code{\link[=peek]{peek()}}, \code{\link[=export]{export()}},
\code{\link[=clear]{clear()}}, \code{\link[=configure_bar]{configure_bar()}}, \code{\link[=par_sapply]{par_sapply()}},
-\code{\link[=par_lapply]{par_lapply()}}, \code{\link[=stop_backend]{stop_backend()}}, and \code{\link{Service}}.
+\code{\link[=par_lapply]{par_lapply()}}, \code{\link[=par_apply]{par_apply()}}, \code{\link[=stop_backend]{stop_backend()}},
+and \code{\link{Service}}.
}
diff --git a/man/export.Rd b/man/export.Rd
index afda767..848eee8 100644
--- a/man/export.Rd
+++ b/man/export.Rd
@@ -90,5 +90,6 @@ backend$active
\seealso{
\code{\link[=start_backend]{start_backend()}}, \code{\link[=peek]{peek()}}, \code{\link[=evaluate]{evaluate()}},
\code{\link[=clear]{clear()}}, \code{\link[=configure_bar]{configure_bar()}}, \code{\link[=par_sapply]{par_sapply()}},
-\code{\link[=par_lapply]{par_lapply()}}, \code{\link[=stop_backend]{stop_backend()}}, and \code{\link{Service}}.
+\code{\link[=par_lapply]{par_lapply()}}, \code{\link[=par_apply]{par_apply()}}, \code{\link[=stop_backend]{stop_backend()}},
+and \code{\link{Service}}.
}
diff --git a/man/par_apply.Rd b/man/par_apply.Rd
new file mode 100644
index 0000000..f52c5f6
--- /dev/null
+++ b/man/par_apply.Rd
@@ -0,0 +1,116 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/exports.r
+\name{par_apply}
+\alias{par_apply}
+\title{Run a Task in Parallel}
+\usage{
+par_apply(backend = NULL, x, margin, fun, ...)
+}
+\arguments{
+\item{backend}{An object of class \code{\link{Backend}} as returned by the
+\code{\link[=start_backend]{start_backend()}} function. It can also be \code{NULL} to run the task
+sequentially via \code{\link[base:apply]{base::apply()}}. The default value is \code{NULL}.}
+
+\item{x}{An array to pass to the \code{fun} function.}
+
+\item{margin}{A numeric vector indicating the dimensions of \code{x} the
+\code{fun} function should be applied over. For example, for a matrix,
+\code{margin = 1} indicates applying \code{fun} rows-wise, \code{margin = 2}
+indicates applying \code{fun} columns-wise, and \code{margin = c(1, 2)}
+indicates applying \code{fun} element-wise. Named dimensions are also
+possible depending on \code{x}. See \code{\link[parallel:clusterApply]{parallel::parApply()}} and
+\code{\link[base:apply]{base::apply()}} for more details.}
+
+\item{fun}{A function to apply to \code{x} according to the \code{margin}.}
+
+\item{...}{Additional arguments to pass to the \code{fun} function.}
+}
+\value{
+The dimensions of the output vary according to the \code{margin} argument. Consult
+the documentation of \code{\link[base:apply]{base::apply()}} for a detailed explanation on how the
+output is structured.
+}
+\description{
+This function can be used to run a task in parallel. The task is executed in
+parallel on the specified backend, similar to \code{\link[parallel:clusterApply]{parallel::parApply()}}. If
+\code{backend = NULL}, the task is executed sequentially using \code{\link[base:apply]{base::apply()}}.
+See the \strong{Details} section for more information on how this function works.
+}
+\details{
+This function uses the \code{\link{UserApiConsumer}} class that acts like an
+interface for the developer API of the \code{\link{parabar}} package.
+}
+\examples{
+\donttest{
+
+# Define a simple task.
+task <- function(x) {
+ # Perform computations.
+ Sys.sleep(0.01)
+
+ # Return the result.
+ mean(x)
+}
+
+# Define a matrix for the task.
+x <- matrix(rnorm(100^2, mean = 10, sd = 0.5), nrow = 100, ncol = 100)
+
+# Start an asynchronous backend.
+backend <- start_backend(cores = 2, cluster_type = "psock", backend_type = "async")
+
+# Run a task in parallel over the rows of `x`.
+results <- par_apply(backend, x = x, margin = 1, fun = task)
+
+# Run a task in parallel over the columns of `x`.
+results <- par_apply(backend, x = x, margin = 2, fun = task)
+
+# The task can also be run over all elements of `x` using `margin = c(1, 2)`.
+# Improper dimensions will throw an error.
+try(par_apply(backend, x = x, margin = c(1, 2, 3), fun = task))
+
+# Disable progress tracking.
+set_option("progress_track", FALSE)
+
+# Run a task in parallel.
+results <- par_apply(backend, x = x, margin = 1, fun = task)
+
+# Enable progress tracking.
+set_option("progress_track", TRUE)
+
+# Change the progress bar options.
+configure_bar(type = "modern", format = "[:bar] :percent")
+
+# Run a task in parallel.
+results <- par_apply(backend, x = x, margin = 1, fun = task)
+
+# Stop the backend.
+stop_backend(backend)
+
+# Start a synchronous backend.
+backend <- start_backend(cores = 2, cluster_type = "psock", backend_type = "sync")
+
+# Run a task in parallel.
+results <- par_apply(backend, x = x, margin = 1, fun = task)
+
+# Disable progress tracking to remove the warning that progress is not supported.
+set_option("progress_track", FALSE)
+
+# Run a task in parallel.
+results <- par_apply(backend, x = x, margin = 1, fun = task)
+
+# Stop the backend.
+stop_backend(backend)
+
+# Run the task using the `base::lapply` (i.e., non-parallel).
+results <- par_apply(NULL, x = x, margin = 1, fun = task)
+
+}
+
+}
+\seealso{
+\code{\link[=start_backend]{start_backend()}}, \code{\link[=peek]{peek()}}, \code{\link[=export]{export()}},
+\code{\link[=evaluate]{evaluate()}}, \code{\link[=clear]{clear()}}, \code{\link[=configure_bar]{configure_bar()}},
+\code{\link[=par_sapply]{par_sapply()}}, \code{\link[=par_lapply]{par_lapply()}}, \code{\link[=stop_backend]{stop_backend()}},
+\code{\link[=set_option]{set_option()}}, \code{\link[=get_option]{get_option()}}, \code{\link{Options}},
+\code{\link{UserApiConsumer}}, and \code{\link{Service}}.
+}
diff --git a/man/par_lapply.Rd b/man/par_lapply.Rd
index ec3d248..c96d46b 100644
--- a/man/par_lapply.Rd
+++ b/man/par_lapply.Rd
@@ -91,7 +91,7 @@ results <- par_lapply(NULL, x = 1:300, fun = task)
\seealso{
\code{\link[=start_backend]{start_backend()}}, \code{\link[=peek]{peek()}}, \code{\link[=export]{export()}},
\code{\link[=evaluate]{evaluate()}}, \code{\link[=clear]{clear()}}, \code{\link[=configure_bar]{configure_bar()}},
-\code{\link[=par_sapply]{par_sapply()}}, \code{\link[=stop_backend]{stop_backend()}}, \code{\link[=set_option]{set_option()}},
-\code{\link[=get_option]{get_option()}}, \code{\link{Options}}, \code{\link{UserApiConsumer}},
-and \code{\link{Service}}.
+\code{\link[=par_sapply]{par_sapply()}}, \code{\link[=par_apply]{par_apply()}}, \code{\link[=stop_backend]{stop_backend()}},
+\code{\link[=set_option]{set_option()}}, \code{\link[=get_option]{get_option()}}, \code{\link{Options}},
+\code{\link{UserApiConsumer}}, and \code{\link{Service}}.
}
diff --git a/man/par_sapply.Rd b/man/par_sapply.Rd
index 3dc5cbd..f94ce5b 100644
--- a/man/par_sapply.Rd
+++ b/man/par_sapply.Rd
@@ -91,7 +91,7 @@ results <- par_sapply(NULL, x = 1:300, fun = task)
\seealso{
\code{\link[=start_backend]{start_backend()}}, \code{\link[=peek]{peek()}}, \code{\link[=export]{export()}},
\code{\link[=evaluate]{evaluate()}}, \code{\link[=clear]{clear()}}, \code{\link[=configure_bar]{configure_bar()}},
-\code{\link[=par_lapply]{par_lapply()}}, \code{\link[=stop_backend]{stop_backend()}}, \code{\link[=set_option]{set_option()}},
-\code{\link[=get_option]{get_option()}}, \code{\link{Options}}, \code{\link{UserApiConsumer}},
-and \code{\link{Service}}.
+\code{\link[=par_lapply]{par_lapply()}}, \code{\link[=par_apply]{par_apply()}}, \code{\link[=stop_backend]{stop_backend()}},
+\code{\link[=set_option]{set_option()}}, \code{\link[=get_option]{get_option()}}, \code{\link{Options}},
+\code{\link{UserApiConsumer}}, and \code{\link{Service}}.
}
diff --git a/man/parabar-package.Rd b/man/parabar-package.Rd
index 5447e12..bb6ac2b 100644
--- a/man/parabar-package.Rd
+++ b/man/parabar-package.Rd
@@ -34,6 +34,10 @@ backend, similar to the built-in function \code{\link[parallel:clusterApply]{par
\code{\link[base:lapply]{base::lapply()}} function when no backend is provided. However, when a
backend is provided, the function will execute a task in parallel on the
backend, similar to the built-in function \code{\link[parallel:clusterApply]{parallel::parLapply()}}.
+\item \code{\link[=par_apply]{par_apply()}}: is a drop-in replacement for the built-in
+\code{\link[base:apply]{base::apply()}} function when no backend is provided. However, when a
+backend is provided, the function will execute a task in parallel on the
+backend, similar to the built-in function \code{\link[parallel:clusterApply]{parallel::parApply()}}.
\item \code{\link[=clear]{clear()}}: removes all variables available on a backend.
\item \code{\link[=peek]{peek()}}: returns the names of all variables available on a
backend.
@@ -76,8 +80,8 @@ The \code{\link{Service}} interface defines the following operations:
\code{\link[=Service]{start()}}, \code{\link[=Service]{stop()}},
\code{\link[=Service]{clear()}}, \code{\link[=Service]{peek()}},
\code{\link[=Service]{export()}}, \code{\link[=Service]{evaluate()}},
-\code{\link[=Service]{sapply()}}, \code{\link[=Service]{lapply()}}, and
-\code{\link[=Service]{get_output()}}.
+\code{\link[=Service]{sapply()}}, \code{\link[=Service]{lapply()}},
+\code{\link[=Service]{apply()}}, and \code{\link[=Service]{get_output()}}.
Check out the documentation for \code{\link{Service}} for more information on
each method.
@@ -110,9 +114,12 @@ asynchronous backend.
\item \code{\link{BackendFactory}}: factory for creating backend objects.
\item \code{\link{Context}}: default context for executing backend operations.
\item \code{\link{ProgressTrackingContext}}: context for decorating the
-\code{\link[=Service]{sapply()}} and \code{\link[=Service]{lapply()}}
+\code{\link[=Service]{sapply()}}, \code{\link[=Service]{lapply()}}, and
+\code{\link[=Service]{apply()}}
operations to track and display the execution progress.
\item \code{\link{ContextFactory}}: factory for creating context objects.
+\item \code{\link{UserApiConsumer}}: opinionated wrapper around the other
+\code{\link[R6:R6Class]{R6::R6}} classes used in by the exported functions for the users.
}
}
}
diff --git a/man/peek.Rd b/man/peek.Rd
index 36dad9f..078edd5 100644
--- a/man/peek.Rd
+++ b/man/peek.Rd
@@ -86,5 +86,6 @@ backend$active
\seealso{
\code{\link[=start_backend]{start_backend()}}, \code{\link[=export]{export()}}, \code{\link[=evaluate]{evaluate()}},
\code{\link[=clear]{clear()}}, \code{\link[=configure_bar]{configure_bar()}}, \code{\link[=par_sapply]{par_sapply()}},
-\code{\link[=par_lapply]{par_lapply()}}, \code{\link[=stop_backend]{stop_backend()}}, and \code{\link{Service}}.
+\code{\link[=par_lapply]{par_lapply()}}, \code{\link[=par_apply]{par_apply()}}, \code{\link[=stop_backend]{stop_backend()}},
+and \code{\link{Service}}.
}
diff --git a/man/start_backend.Rd b/man/start_backend.Rd
index cd7eb0f..646a59c 100644
--- a/man/start_backend.Rd
+++ b/man/start_backend.Rd
@@ -130,5 +130,6 @@ backend$active
\seealso{
\code{\link[=peek]{peek()}}, \code{\link[=export]{export()}}, \code{\link[=evaluate]{evaluate()}},
\code{\link[=clear]{clear()}}, \code{\link[=configure_bar]{configure_bar()}}, \code{\link[=par_sapply]{par_sapply()}},
-\code{\link[=par_lapply]{par_lapply()}}, \code{\link[=stop_backend]{stop_backend()}}, and \code{\link{Service}}.
+\code{\link[=par_lapply]{par_lapply()}}, \code{\link[=par_apply]{par_apply()}}, \code{\link[=stop_backend]{stop_backend()}},
+and \code{\link{Service}}.
}
diff --git a/man/stop_backend.Rd b/man/stop_backend.Rd
index e36356f..45c4ca2 100644
--- a/man/stop_backend.Rd
+++ b/man/stop_backend.Rd
@@ -89,5 +89,6 @@ backend$active
\seealso{
\code{\link[=start_backend]{start_backend()}}, \code{\link[=peek]{peek()}}, \code{\link[=export]{export()}},
\code{\link[=evaluate]{evaluate()}}, \code{\link[=clear]{clear()}}, \code{\link[=configure_bar]{configure_bar()}},
-\code{\link[=par_sapply]{par_sapply()}}, \code{\link[=par_lapply]{par_lapply()}}, and \code{\link{Service}}.
+\code{\link[=par_sapply]{par_sapply()}}, \code{\link[=par_apply]{par_apply()}}, \code{\link[=par_lapply]{par_lapply()}}, and
+\code{\link{Service}}.
}
diff --git a/tests/testthat/helpers.R b/tests/testthat/helpers.R
index 7f970b5..a84f42d 100644
--- a/tests/testthat/helpers.R
+++ b/tests/testthat/helpers.R
@@ -110,6 +110,9 @@ tests_set_for_unimplemented_service_methods <- function(service) {
# Expect an error when calling the `lapply` method.
expect_error(service$lapply(), as_text(Exception$method_not_implemented()))
+ # Expect an error when calling the `apply` method.
+ expect_error(service$apply(), as_text(Exception$method_not_implemented()))
+
# Expect an error when calling the `get_output` method.
expect_error(service$get_output(), as_text(Exception$method_not_implemented()))
}
@@ -239,6 +242,60 @@ tests_set_for_synchronous_backend_operations <- function(service, specification,
# Tests for the `lapply` operation.
tests_set_for_synchronous_backend_task_execution(operation, service, expected_output)
+ # Redefine `x` as a matrix for the `apply` operation.
+ x <- matrix(rnorm(100^2), nrow = 100, ncol = 100)
+
+ # Compute the expected output for the `apply` operation applied over rows.
+ expected_output <- base::apply(x, 1, task, y = y, z = z)
+
+ # Define the `apply` operation over rows.
+ operation <- bquote(service$apply(.(x), 1, .(task), y = .(y), z = .(z), sleep = .(sleep)))
+
+ # Tests for the `apply` operation over rows.
+ tests_set_for_synchronous_backend_task_execution(operation, service, expected_output)
+
+ # Compute the expected output for the `apply` operation applied over columns.
+ expected_output <- base::apply(x, 2, task, y = y, z = z)
+
+ # Define the `apply` operation over columns.
+ operation <- bquote(service$apply(.(x), 2, .(task), y = .(y), z = .(z), sleep = .(sleep)))
+
+ # Tests for the `apply` operation over columns.
+ tests_set_for_synchronous_backend_task_execution(operation, service, expected_output)
+
+ # Redefine a smaller `x` matrix for the `apply` operation applied element-wise.
+ x <- matrix(rnorm(10^2), nrow = 10, ncol = 10)
+
+ # Compute the expected output for the `apply` operation applied element-wise.
+ expected_output <- base::apply(x, c(1, 2), task, y = y, z = z)
+
+ # Define the `apply` operation element-wise.
+ operation <- bquote(service$apply(.(x), c(1, 2), .(task), y = .(y), z = .(z), sleep = .(sleep)))
+
+ # Tests for the `apply` operation element-wise.
+ tests_set_for_synchronous_backend_task_execution(operation, service, expected_output)
+
+ # Expect error when margins higher than the array dimensions are provided.
+ expect_error(
+ service$apply(x, 3, task, y = y, z = z),
+ as_text(Exception$array_margins_not_compatible(3, dim(x))),
+ fixed = TRUE
+ )
+
+ # Expect error when duplicate margins are provided.
+ expect_error(
+ service$apply(x, c(1, 1), task, y = y, z = z),
+ as_text(Exception$array_margins_not_compatible(c(1, 1), dim(x))),
+ fixed = TRUE
+ )
+
+ # Expect errors when the margins are not compatible with the array dimensions.
+ expect_error(
+ service$apply(x, c(1, 2, 3, 1), task, y = y, z = z),
+ as_text(Exception$array_margins_not_compatible(c(1, 2, 3, 1), dim(x))),
+ fixed = TRUE
+ )
+
# Expect that the cluster is empty after performing operations on it.
expect_true(all(sapply(service$peek(), length) == 0))
@@ -341,6 +398,60 @@ tests_set_for_asynchronous_backend_operations <- function(service, specification
# Tests for the `lapply` operation.
tests_set_for_asynchronous_backend_task_execution(operation, service, expected_output)
+ # Redefine `x` as a matrix for the `apply` operation.
+ x <- matrix(rnorm(100^2), nrow = 100, ncol = 100)
+
+ # Compute the expected output for the `apply` operation applied over rows.
+ expected_output <- base::apply(x, 1, task, y = y, z = z)
+
+ # Define the `apply` operation over rows.
+ operation <- bquote(service$apply(.(x), 1, .(task), y = .(y), z = .(z), sleep = .(sleep)))
+
+ # Tests for the `apply` operation over rows.
+ tests_set_for_asynchronous_backend_task_execution(operation, service, expected_output)
+
+ # Compute the expected output for the `apply` operation applied over columns.
+ expected_output <- base::apply(x, 2, task, y = y, z = z)
+
+ # Define the `apply` operation over columns.
+ operation <- bquote(service$apply(.(x), 2, .(task), y = .(y), z = .(z), sleep = .(sleep)))
+
+ # Tests for the `apply` operation over columns.
+ tests_set_for_asynchronous_backend_task_execution(operation, service, expected_output)
+
+ # Redefine a smaller `x` matrix for the `apply` operation applied element-wise.
+ x <- matrix(rnorm(10^2), nrow = 10, ncol = 10)
+
+ # Compute the expected output for the `apply` operation applied element-wise.
+ expected_output <- base::apply(x, c(1, 2), task, y = y, z = z)
+
+ # Define the `apply` operation element-wise.
+ operation <- bquote(service$apply(.(x), c(1, 2), .(task), y = .(y), z = .(z), sleep = .(sleep)))
+
+ # Tests for the `apply` operation element-wise.
+ tests_set_for_asynchronous_backend_task_execution(operation, service, expected_output)
+
+ # Expect error when margins higher than the array dimensions are provided.
+ expect_error(
+ service$apply(x, 3, task, y = y, z = z),
+ as_text(Exception$array_margins_not_compatible(3, dim(x))),
+ fixed = TRUE
+ )
+
+ # Expect error when duplicate margins are provided.
+ expect_error(
+ service$apply(x, c(1, 1), task, y = y, z = z),
+ as_text(Exception$array_margins_not_compatible(c(1, 1), dim(x))),
+ fixed = TRUE
+ )
+
+ # Expect errors when the margins are not compatible with the array dimensions.
+ expect_error(
+ service$apply(x, c(1, 2, 3, 1), task, y = y, z = z),
+ as_text(Exception$array_margins_not_compatible(c(1, 2, 3, 1), dim(x))),
+ fixed = TRUE
+ )
+
# Expect that the cluster is empty after performing operations on it.
expect_true(all(sapply(service$peek(), length) == 0))
@@ -436,6 +547,39 @@ tests_set_for_progress_tracking_context <- function(context, task) {
# Tests for the `lapply` operation in a progress tracking context.
tests_set_for_task_execution_with_progress_tracking(operation, context, expected_output)
+
+ # Redefine `x` as a matrix for the `apply` operation.
+ x <- matrix(rnorm(100^2), nrow = 100, ncol = 100)
+
+ # Compute the expected output for the `apply` operation applied over rows.
+ expected_output <- base::apply(x, 1, task, y = y, z = z)
+
+ # Define the `apply` operation over rows.
+ operation <- bquote(context$apply(.(x), 1, .(task), y = .(y), z = .(z), sleep = .(sleep)))
+
+ # Tests for the `apply` operation over rows in a progress tracking context.
+ tests_set_for_task_execution_with_progress_tracking(operation, context, expected_output)
+
+ # Compute the expected output for the `apply` operation applied over columns.
+ expected_output <- base::apply(x, 2, task, y = y, z = z)
+
+ # Define the `apply` operation over columns.
+ operation <- bquote(context$apply(.(x), 2, .(task), y = .(y), z = .(z), sleep = .(sleep)))
+
+ # Tests for the `apply` operation over columns in a progress tracking context.
+ tests_set_for_task_execution_with_progress_tracking(operation, context, expected_output)
+
+ # Redefine a smaller `x` matrix for the `apply` operation applied element-wise.
+ x <- matrix(rnorm(10^2), nrow = 10, ncol = 10)
+
+ # Compute the expected output for the `apply` operation applied element-wise.
+ expected_output <- base::apply(x, c(1, 2), task, y = y, z = z)
+
+ # Define the `apply` operation element-wise.
+ operation <- bquote(context$apply(.(x), c(1, 2), .(task), y = .(y), z = .(z), sleep = .(sleep)))
+
+ # Tests for the `apply` operation over all elements in a progress tracking context.
+ tests_set_for_task_execution_with_progress_tracking(operation, context, expected_output)
}
#endregion
@@ -705,6 +849,19 @@ ProgressTrackingContextTester <- R6::R6Class("ProgressTrackingContextTester",
private$.execute_and_capture_progress(operation)
},
+ # Implementation for the `apply` method capturing the progress output.
+ apply = function(x, margin, fun, ...) {
+ # Define the operation.
+ operation <- bquote(
+ do.call(
+ super$apply, c(list(.(x), .(margin), .(fun)), .(list(...)))
+ )
+ )
+
+ # Execute the task via the operation and capture the progress output.
+ private$.execute_and_capture_progress(operation)
+ },
+
# Wrapper to expose `.make_log` for testing.
make_log = function() {
private$.make_log()
diff --git a/tests/testthat/test-user-api.R b/tests/testthat/test-user-api.R
index e27fe9e..d42ecc5 100644
--- a/tests/testthat/test-user-api.R
+++ b/tests/testthat/test-user-api.R
@@ -231,6 +231,11 @@ test_that("user API functions handle incompatible input correctly", {
par_lapply(backend, NULL, NULL),
as_text(Exception$type_not_assignable(class(backend), "Backend"))
)
+
+ expect_error(
+ par_apply(backend, NULL, NULL, NULL),
+ as_text(Exception$type_not_assignable(class(backend), "Backend"))
+ )
})
@@ -321,6 +326,28 @@ test_that("user API functions run tasks in parallel correctly", {
# Expect the `par_lapply` to run the task in parallel correctly.
tests_set_for_user_api_task_execution(parallel_lapply, sequential_lapply, expected_output)
+
+ # Redefine `x` as a matrix for the `apply` operation.
+ x <- matrix(rnorm(100^2), nrow = 100, ncol = 100)
+
+ # Select a random margin for the matrix.
+ margin <- sample(1:2, 1)
+
+ # Compute the expected output for the `par_apply` user API function.
+ expected_output <- base::apply(x, margin, test_task, y = y, z = z)
+
+ # Define the `par_apply` parallel operation.
+ parallel_apply <- bquote(
+ par_apply(backend, x = .(x), margin = .(margin), fun = test_task, .(y), .(z), sleep = .(sleep))
+ )
+
+ # Define the `par_apply` sequential operation.
+ sequential_apply <- bquote(
+ par_apply(backend = NULL, x = .(x), margin = .(margin), fun = test_task, .(y), .(z))
+ )
+
+ # Expect the `par_apply` to run the task in parallel correctly.
+ tests_set_for_user_api_task_execution(parallel_apply, sequential_apply, expected_output)
})
@@ -336,6 +363,18 @@ test_that("user API functions track progress correctly", {
tests_set_for_user_api_progress_tracking(bquote(
par_lapply(backend, x = 1:100, fun = test_task, 1, 2)
))
+
+ # Create a matrix for the `apply` operation.
+ x <- matrix(rnorm(50^2), nrow = 50, ncol = 50)
+
+ # Select a random margin for the matrix.
+ margin <- sample(1:2, sample(1:2, 1))
+
+ # Expect progress tracking is displayed correctly via `par_apply`.
+ tests_set_for_user_api_progress_tracking(bquote(
+ par_apply(backend, x = .(x), margin = .(margin), fun = test_task, 1, 2)
+ ))
+
} else {
skip("Test only runs in interactive contexts.")
}