--- title: "Annotated parameter values" output: rmarkdown::html_vignette: toc: true toc_depth: 4 description: > Shows how to store annotated values (e.g. values with units or labels) as step parameters and how to unwrap them right before a step function is called, using the pip_param_value() generic. vignette: > %\VignetteIndexEntry{Annotated parameter values} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r knitr-setup, include = FALSE} knitr::opts_chunk$set( comment = "#", prompt = FALSE, tidy = FALSE, cache = FALSE, collapse = TRUE ) old <- options(width = 100L) ``` Parameters of a pipeline are usually plain values such as numbers or strings. Some packages, however, store *annotated* values as parameters: numbers with a unit, values with a label and a description, or parameter objects that carry metadata for a user interface. The step functions usually need the plain value, though. Without further help, every step function of such a pipeline would have to unwrap its arguments itself, or be wrapped in a function that does so. The generic `pip_param_value()` is a single hook for this. You write one method for your class, and the pipeline applies it whenever a step is run. ### An annotated value Let's use a simple class that holds a number together with its unit. ```{r quantity-class} library(pipeflow) quantity <- function(value, unit) { structure(list(value = value, unit = unit), class = "quantity") } print.quantity <- function(x, ...) { cat("", x$value, x$unit, "\n") invisible(x) } quantity(120, "km") ``` We use it as the parameters of a step that computes a speed. ```{r speed-pipeline} p <- pip_new("speed") |> pip_add("speed", \(distance = 1, time = 1) distance / time) pip_set_params( p, list(distance = quantity(120, "km"), time = quantity(2, "h")) ) ``` Running the pipeline fails, because the step function receives the `quantity` objects, not the numbers: ```{r speed-without-method} tryCatch( pip_run(p, lgr = NULL), error = \(e) conditionMessage(e) ) ``` ### Write and register a method `pip_param_value()` is an S3 generic, which has a default method that returns the value unchanged. For our class we add a method that returns the plain value. ```{r quantity-method} pip_param_value.quantity <- function(x, ...) x$value ``` Inside a script, register the method with `registerS3method()`: ```{r register-method} registerS3method("pip_param_value", "quantity", pip_param_value.quantity) ``` In a package, register it in the `NAMESPACE` file instead, either directly with `S3method(pipeflow::pip_param_value, quantity)` or, if you use roxygen, by tagging the method with `@exportS3Method pipeflow::pip_param_value`. Methods are meant for your own classes, not for base classes such as `numeric` or `character`. Now the pipeline runs: ```{r speed-with-method} pip_run(p, lgr = NULL) p[["speed", "out"]] ``` The step function received the plain numbers, while the stored parameters still hold the annotated values. ```{r stored-params} pip_get_params(p) ``` ### Where the method is applied The generic is applied once to each *unbound* argument of a step right before the step function is called. This includes extra parameters passed on via `...`: ```{r defaults-and-dots} p2 <- pip_new("defaults") |> pip_add( "describe", \(x = quantity(3, "m"), ...) { c(x = class(x), dots = sapply(list(...), class)) }, params = list(extra = quantity(1, "s")) ) pip_run(p2, lgr = NULL) p2[["describe", "out"]] ``` The generic is *not* applied to * the outputs of other steps, that is, arguments defined as `~step`, * the partitions of a split step, and * failure objects (see `pip_run(on_error = "continue")`). Step outputs, for example, are passed on as they are: ```{r step-outputs} p3 <- pip_new("outputs") |> pip_add("make", \(v = 3) quantity(v, "m")) |> pip_add("inspect", \(q = ~make) class(q)) pip_run(p3, lgr = NULL) p3[["inspect", "out"]] ``` The `quantity` that `make` returns reaches `inspect` untouched. ### Errors in a method A method runs as part of the step call. An error in a method is therefore an error of the step, handled like any other step error as described in the vignette on [handling failures](v05c-error-handling.html). Let's add a class that checks its unit. ```{r checked-class} checked <- function(value, unit) { structure(list(value = value, unit = unit), class = "checked") } pip_param_value.checked <- function(x, ...) { if (!x$unit %in% c("m", "km")) { stop("unknown unit '", x$unit, "'") } x$value } registerS3method("pip_param_value", "checked", pip_param_value.checked) p4 <- pip_new("errors") |> pip_add("double", \(x = 1) x * 2) |> pip_add("increment", \(y = ~double) y + 1) pip_set_params(p4, list(x = checked(1, "ft"))) ``` By default, the run stops and the message tells which step failed: ```{r method-error-stop} tryCatch( pip_run(p4, lgr = NULL), error = \(e) conditionMessage(e) ) ``` With `on_error = "continue"`, the step is marked as failed and the run goes on with the steps that don't depend on it. The run also signals a `pipeflow_run_failed` warning, which we suppress here. ```{r method-error-continue} suppressWarnings( pip_run(p4, lgr = NULL, on_error = "continue") ) pip_data(p4)[, c("step", "state")] ``` As usual, the condition of the failed step is stored as its output. ```{r method-error-condition} conditionMessage(p4[["double", "out"]]) ``` ### Summary * `pip_param_value(x)` unwraps a stored parameter value right before the step function is called, and it does not change the stored parameters. * Define one method per annotated class and register it as an S3 method. * It applies to unbound arguments only (`params`, defaults, and `...`), not to step outputs, partitions or failure objects. * An error in a method is a step error, so `on_error` applies. ```{r, include = FALSE} options(old) ```