|
8 | 8 | #' unevaluated and evaluated forms of arguments passed to a parent function. It |
9 | 9 | #' returns a tibble with columns reflecting argument states: when unevaluated |
10 | 10 | #' and evaluated values differ, columns are named with `_orig` and `_eval` |
11 | | -#' suffixes; when they are the same, a single column is used with the argument |
12 | | -#' name alone. The function dynamically handles scalars, call objects, and |
13 | | -#' complex objects (e.g., `lm` models, `SpatRaster` objects), preserving their |
14 | | -#' structure appropriately. |
| 11 | +#' suffixes; when they are the same (including symbols evaluated to scalars), a |
| 12 | +#' single column is used with the argument name alone. The function dynamically |
| 13 | +#' handles scalars, call objects, and complex objects (e.g., `lm` models, |
| 14 | +#' `SpatRaster` objects), preserving their structure appropriately. |
15 | 15 | #' |
16 | 16 | #' @param ExportPath Character. The path to an `.RData` file where the tibble |
17 | 17 | #' will be exported. If `NULL` (default), the tibble is returned without |
18 | 18 | #' saving. If provided, the tibble is saved to the specified file and `NULL` |
19 | 19 | #' is returned invisibly. |
| 20 | +#' @param call Language object (optional). The call to the parent function, as |
| 21 | +#' provided by `match.call()` from the caller. If `NULL` (default), the |
| 22 | +#' function falls back to `sys.call(-1)` to capture the parent call. Used to |
| 23 | +#' ensure accurate argument capture in iterative contexts (e.g., `lapply`, |
| 24 | +#' `purrr::map`). |
| 25 | +#' @param env Environment (optional). The environment in which to evaluate the |
| 26 | +#' arguments, typically provided by `parent.frame()` from the caller. If |
| 27 | +#' `NULL` (default), the function uses `parent.frame()` to determine the |
| 28 | +#' evaluation environment. Used to resolve variables in iterative contexts. |
20 | 29 | #' |
21 | | -#' @details This function must be called from within another function. It uses |
22 | | -#' `sys.call(-1)` to capture the parent function’s call, evaluates arguments |
23 | | -#' in the parent environment, and combines them with default values from the |
24 | | -#' parent function’s formal arguments. Unevaluated expressions (e.g., `a + b`) |
25 | | -#' are preserved as `call` objects, while evaluated values are kept as-is for |
26 | | -#' scalars or wrapped in lists for complex objects. Columns are ordered based |
27 | | -#' on the original argument sequence: single columns (for matching values) |
28 | | -#' appear first, followed by `_orig` and `_eval` pairs (for differing values) |
| 30 | +#' @details This function must be called from within another function. It |
| 31 | +#' captures the parent function’s call using either a provided `call` argument |
| 32 | +#' or `sys.call(-1)`, evaluates arguments in the specified or default parent |
| 33 | +#' environment, and combines them with default values from the parent |
| 34 | +#' function’s formal arguments. Unevaluated expressions (e.g., `a + b`) are |
| 35 | +#' preserved as character strings via `deparse()`, while scalars (including |
| 36 | +#' symbols like `i` in loops that evaluate to scalars) and complex objects |
| 37 | +#' (e.g., `lm`, `SpatRaster`) are handled appropriately: |
| 38 | +#' - Symbols (e.g., `i` in `lapply`) are treated as matching their evaluated |
| 39 | +#' scalar values, resulting in a single column. |
| 40 | +#' - Calls (e.g., `a + b`) result in `_orig`/`_eval` pairs. |
| 41 | +#' - Complex objects are wrapped in lists in `_eval` columns. |
| 42 | +#' Columns are ordered based on the original argument sequence: single columns |
| 43 | +#' (for matching values) appear first, followed by `_orig` and `_eval` pairs |
29 | 44 | #' in that order. |
30 | 45 | #' |
31 | 46 | #' @return A `tibble` containing the unevaluated and evaluated forms of the |
32 | 47 | #' parent function’s arguments. Column naming depends on whether unevaluated |
33 | 48 | #' and evaluated values match: |
34 | 49 | #' - **Single columns** (e.g., `y`): Used when unevaluated and evaluated |
35 | | -#' values are identical (e.g., scalars like `2` or defaults like `10`), |
| 50 | +#' values are identical or effectively equivalent (e.g., scalars like `2`, |
| 51 | +#' defaults like `10`, or symbols like `i` evaluating to `1` in loops), |
36 | 52 | #' containing the evaluated value as-is. |
37 | 53 | #' - **Paired columns** (e.g., `x_orig`, `x_eval`): |
38 | | -#' - `*_orig`: Unevaluated expressions as list columns with `call` objects |
39 | | -#' (e.g., `a + b`) or scalars as-is. |
| 54 | +#' - `*_orig`: Unevaluated expressions as character strings (e.g., |
| 55 | +#' `"a + b"`) or scalars as-is for non-call objects. |
40 | 56 | #' - `*_eval`: Evaluated values, either scalars (e.g., `8`) or list columns |
41 | 57 | #' for complex objects (e.g., `lm`, `SpatRaster`). |
42 | 58 | #' |
|
46 | 62 | #' |
47 | 63 | #' @author Ahmed El-Gabbas |
48 | 64 | #' @export |
| 65 | +#' @name RecordArgs |
49 | 66 | #' @examples |
50 | 67 | #' a <- 5 |
51 | 68 | #' b <- 3 |
| 69 | +#' |
52 | 70 | #' Function1 <- function(w = 5, x, y, z = 10) { |
53 | | -#' Args <- RecordArgs() |
| 71 | +#' Args <- IASDT.R::RecordArgs(call = match.call(), env = parent.frame()) |
54 | 72 | #' return(Args) |
55 | 73 | #' } |
56 | 74 | #' |
57 | 75 | #' # -------------------------------------------------------------- |
58 | 76 | #' |
59 | 77 | #' # Basic usage with scalars and expressions |
60 | 78 | #' Out1 <- Function1(x = a + b, y = 2) |
61 | | -#' Out1$w # 5 (single column, same as orig and eval) |
62 | | -#' Out1$x_orig # call object: a + b |
63 | | -#' Out1$x_eval # 8 |
64 | | -#' Out1$y # 2 (single column) |
65 | | -#' Out1$z # 10 (single column) |
| 79 | +#' |
| 80 | +#' Out1 |
| 81 | +#' |
| 82 | +#' Out1$w # 5 (single column, default matches evaluated) |
| 83 | +#' Out1$x_orig # "a + b" (unevaluated expression) |
| 84 | +#' Out1$x_eval # 8 (evaluated result) |
| 85 | +#' Out1$y # 2 (single column, scalar matches evaluated) |
| 86 | +#' Out1$z # 10 (single column, default matches evaluated) |
66 | 87 | #' |
67 | 88 | #' # -------------------------------------------------------------- |
68 | 89 | #' |
69 | | -#' #' # Usage with complex objects (lm and SpatRaster) |
| 90 | +#' # Usage with complex objects (lm and Raster) |
70 | 91 | #' Out2 <- Function1( |
71 | 92 | #' w = 10, |
72 | 93 | #' x = a + b, |
73 | 94 | #' y = stats::lm(mpg ~ disp + hp, data = mtcars), |
74 | | -#' z = terra::rast(system.file("ex/logo.tif", package = "terra"))) |
| 95 | +#' z = raster::raster()) |
| 96 | +#' |
| 97 | +#' Out2 |
| 98 | +#' |
75 | 99 | #' Out2$w # 10 (single column) |
76 | | -#' Out2$x_orig # call object: a + b |
77 | | -#' Out2$x_eval # 8 |
78 | | -#' Out2$y_orig # call object: lm(mpg ~ disp + hp, data = mtcars) |
| 100 | +#' Out2$x_orig # "a + b" (unevaluated expression) |
| 101 | +#' Out2$x_eval # 8 (evaluated result) |
| 102 | +#' Out2$y_orig # "stats::lm(mpg ~ disp + hp, data = mtcars)" |
79 | 103 | #' Out2$y_eval[[1]] # lm object |
80 | | -#' Out2$z_orig # call object: terra::rast(system.file(...)) |
81 | | -#' Out2$z_eval[[1]] # SpatRaster object |
| 104 | +#' Out2$z_orig # "raster::raster()" |
| 105 | +#' Out2$z_eval[[1]] # RasterLayer object |
82 | 106 | #' |
| 107 | +#' # -------------------------------------------------------------- |
| 108 | +#' |
| 109 | +#' # Usage with purrr::pmap for multiple inputs |
| 110 | +#' w_values <- 1:3 |
| 111 | +#' x_values <- c(a + b, 10, 15) |
| 112 | +#' y_values <- c("ABCD", "XYZ123", "TEST") |
| 113 | +#' Out3 <- purrr::pmap( |
| 114 | +#' .l = list(w = w_values, x = x_values, y = y_values), |
| 115 | +#' .f = function(w, x, y) { |
| 116 | +#' Function1( |
| 117 | +#' w = w, |
| 118 | +#' x = x, |
| 119 | +#' y = stringr::str_extract(y, "B.+$"), |
| 120 | +#' z = terra::rast(system.file("ex/elev.tif", package="terra"))) |
| 121 | +#' }) %>% |
| 122 | +#' dplyr::bind_rows() |
| 123 | +#' |
| 124 | +#' Out3 |
| 125 | +#' |
| 126 | +#' Out3$w # 1, 2, 3 |
| 127 | +#' Out3$x # 8, 10, 15 |
| 128 | +#' Out3$y_orig # 'stringr::str_extract(y, "B.+$")', repeated for each row |
| 129 | +#' Out3$y_eval # "BCD", NA, NA |
| 130 | +#' Out3$z_orig # "terra::rast(...))", repeated for each row |
| 131 | +#' Out3$z_eval # Packed SpatRaster, repeated for each row |
| 132 | + |
| 133 | +RecordArgs <- function(ExportPath = NULL, call = NULL, env = NULL) { |
83 | 134 |
|
84 | | -RecordArgs <- function(ExportPath = NULL) { |
85 | | - # Get the call to the parent function (one level up) |
86 | | - call_info <- sys.call(-1) |
| 135 | + # Capture the parent function's call: use provided call (e.g., from |
| 136 | + # match.call()) or fall back to sys.call(-1) for direct calls |
| 137 | + call_info <- if (!is.null(call)) call else sys.call(-1) |
87 | 138 |
|
| 139 | + # Check if call_info is valid; stop if not called within a function |
88 | 140 | if (is.null(call_info)) { |
89 | 141 | stop( |
90 | 142 | "RecordArgs() must be called from within another function", call. = FALSE) |
91 | 143 | } |
92 | 144 |
|
93 | | - # Extract the arguments, excluding the function name |
| 145 | + # Extract the arguments from the call, excluding the function name (first |
| 146 | + # element) |
94 | 147 | args_list <- as.list(call_info)[-1] |
95 | 148 |
|
96 | | - # Extract the name of the calling function |
| 149 | + # Get the name of the calling function as a character string |
97 | 150 | calling_func <- deparse(call_info[[1]]) |
98 | 151 |
|
99 | | - # Get the parent function's environment and formal arguments |
100 | | - parent_env <- parent.frame() |
| 152 | + # Determine the environment for evaluation: use provided env (e.g., from |
| 153 | + # parent.frame()) or default to the immediate parent environment |
| 154 | + parent_env <- if (!is.null(env)) env else parent.frame() |
| 155 | + |
| 156 | + # Retrieve the parent function and its formal arguments (including defaults) |
101 | 157 | parent_func <- sys.function(-1) |
102 | 158 | formals_full <- formals(parent_func) |
103 | 159 |
|
104 | | - # Evaluate the arguments in the parent environment |
| 160 | + # Evaluate the captured arguments in the parent environment |
105 | 161 | args_values <- lapply(args_list, eval, envir = parent_env) |
| 162 | + |
| 163 | + # Name the evaluated values with their corresponding argument names |
106 | 164 | recorded_values <- stats::setNames(args_values, names(args_list)) |
107 | | - # Combine with default values |
| 165 | + |
| 166 | + # Merge evaluated values with defaults, overriding defaults with provided |
| 167 | + # values |
108 | 168 | Evaluated <- utils::modifyList(formals_full, recorded_values) |
109 | | - # Store unevaluated expressions |
| 169 | + |
| 170 | + # Merge unevaluated expressions with defaults, keeping unevaluated forms |
110 | 171 | Unevaluated <- utils::modifyList(formals_full, args_list) |
111 | 172 |
|
112 | | - # Get argument names in their original order |
| 173 | + # Get the argument names in their original order from the function definition |
113 | 174 | arg_names <- names(formals_full) |
114 | 175 |
|
115 | | - # Determine which arguments have identical unevaluated and evaluated values |
| 176 | + # Identify which arguments have identical unevaluated and evaluated values |
| 177 | + # - Calls (e.g., a + b) are always different |
| 178 | + # - Symbols (e.g., i in loops) are treated as matching their evaluated scalar |
| 179 | + # - Scalars and defaults are compared directly |
116 | 180 | same_values <- purrr::map2_lgl( |
117 | | - .x = Unevaluated, |
118 | | - .y = Evaluated, |
| 181 | + # Coerce pairlist to list for purrr compatibility |
| 182 | + .x = as.list(Unevaluated), |
| 183 | + .y = as.list(Evaluated), |
119 | 184 | .f = function(u, e) { |
120 | | - if (is.call(u)) return(FALSE) # Calls always differ from evaluated |
| 185 | + # Calls always differ from evaluated |
| 186 | + if (is.call(u)) return(FALSE) |
| 187 | + # Symbols match their evaluated value |
| 188 | + if (is.symbol(u)) return(identical(e, e)) |
| 189 | + # Direct comparison for scalars and defaults |
121 | 190 | identical(u, e) |
122 | 191 | }) |
123 | 192 |
|
124 | | - # Names for single columns |
| 193 | + # Define column names: single columns for matching values, pairs for differing |
| 194 | + # ones |
| 195 | + # |
| 196 | + # Names for arguments with identical values |
125 | 197 | single_cols <- arg_names[same_values] |
126 | | - # Names for orig/eval pairs |
| 198 | + # Names for arguments needing _orig/_eval pairs |
127 | 199 | diff_cols <- arg_names[!same_values] |
128 | 200 |
|
129 | | - # Construct column names |
130 | | - single_cols <- single_cols # e.g., "y", "z" |
131 | | - eval_cols <- paste0(diff_cols, "_eval") # e.g., "x_eval" |
132 | | - uneval_cols <- paste0(diff_cols, "_orig") # e.g., "x_orig" |
| 201 | + # Construct column names for the tibble |
133 | 202 |
|
134 | | - # Prepare Evaluated values: keep scalars as-is, wrap complex objects in lists |
| 203 | + # e.g., "y", "z" (unchanged) |
| 204 | + single_cols <- single_cols |
| 205 | + # e.g., "x_eval" (evaluated values) |
| 206 | + eval_cols <- paste0(diff_cols, "_eval") |
| 207 | + # e.g., "x_orig" (unevaluated forms) |
| 208 | + uneval_cols <- paste0(diff_cols, "_orig") |
| 209 | + |
| 210 | + # Format evaluated values: scalars as-is, complex objects (e.g., SpatRaster) |
| 211 | + # wrapped in lists |
135 | 212 | eval_values <- purrr::map( |
136 | 213 | .x = as.list(Evaluated), |
137 | 214 | .f = function(x) { |
138 | 215 | if (is.vector(x) && length(x) == 1 && !is.list(x)) { |
| 216 | + # Scalars remain as-is (e.g., 8, "BCD") |
139 | 217 | return(x) |
140 | 218 | } else { |
141 | 219 | if (inherits(x, "SpatRaster")) { |
| 220 | + # Wrap SpatRaster objects for storage |
142 | 221 | x <- terra::wrap(x) |
143 | 222 | } |
| 223 | + # Wrap complex objects (e.g., lm, RasterLayer) in a list |
144 | 224 | return(list(x)) |
145 | 225 | } |
146 | 226 | }) |
147 | 227 |
|
148 | | - # Prepare Unevaluated values: keep calls as-is, wrap in list, scalars stay |
149 | | - # as-is |
| 228 | + # Format unevaluated values: calls as character strings, scalars as-is, |
| 229 | + # complex objects in lists |
150 | 230 | uneval_values <- purrr::map( |
151 | 231 | .x = as.list(Unevaluated), |
152 | 232 | .f = function(x) { |
153 | 233 | if (is.call(x)) { |
154 | | - return(list(x)) |
| 234 | + # Convert calls (e.g., a + b) to strings without quotes |
| 235 | + return(noquote(deparse(x))) |
155 | 236 | } else if (is.vector(x) && length(x) == 1 && !is.list(x)) { |
| 237 | + # Scalars (e.g., 2, 10) remain as-is |
156 | 238 | return(x) |
157 | 239 | } else { |
158 | 240 | if (inherits(x, "SpatRaster")) { |
| 241 | + # Wrap SpatRaster objects |
159 | 242 | x <- terra::wrap(x) |
160 | 243 | } |
| 244 | + # Wrap complex objects in a list |
161 | 245 | return(list(x)) |
162 | 246 | } |
163 | 247 | }) |
164 | 248 |
|
165 | | - # Combine into a tibble |
| 249 | + # Combine all values into a named list for tibble construction |
166 | 250 | tibble_data <- c( |
| 251 | + # Unevaluated differing values |
167 | 252 | stats::setNames(uneval_values[diff_cols], uneval_cols), |
| 253 | + # Evaluated differing values |
168 | 254 | stats::setNames(eval_values[diff_cols], eval_cols), |
169 | | - stats::setNames(eval_values[single_cols], single_cols) |
170 | | - ) |
| 255 | + # Single columns for matching values |
| 256 | + stats::setNames(eval_values[single_cols], single_cols)) |
171 | 257 |
|
172 | | - # Create tibble and reorder columns by argument order |
| 258 | + # Create the tibble from the combined data |
173 | 259 | result <- tibble::as_tibble(tibble_data) |
174 | 260 |
|
175 | | - # Define the desired column order: single columns first, then orig/eval pairs |
176 | | - desired_order <- c( |
177 | | - single_cols, |
178 | | - unlist(lapply( |
179 | | - diff_cols, |
180 | | - function(n) { |
181 | | - c(paste0(n, "_orig"), paste0(n, "_eval")) |
182 | | - } |
183 | | - )) |
184 | | - ) |
185 | | - |
186 | | - result <- dplyr::select(result, tidyselect::all_of(desired_order)) |
| 261 | + # Reorder the tibble columns according to the desired order |
| 262 | + # Define the desired column order |
| 263 | + desired_order <- purrr::map( |
| 264 | + arg_names, ~ c(.x, paste0(.x, "_orig"), paste0(.x, "_eval"))) %>% |
| 265 | + unlist() |
| 266 | + result <- dplyr::select(result, tidyselect::any_of(desired_order)) |
187 | 267 |
|
188 | | - # Export the tibble to a file if a path is provided |
| 268 | + # Return the tibble or save it to a file based on ExportPath |
189 | 269 | if (is.null(ExportPath)) { |
| 270 | + # Return the tibble if no export path is provided |
190 | 271 | return(result) |
191 | 272 | } else { |
| 273 | + # Save to .RData file if ExportPath is specified |
192 | 274 | IASDT.R::SaveAs( |
193 | 275 | InObj = result, OutObj = paste0("Args_", calling_func), |
194 | 276 | OutPath = ExportPath) |
| 277 | + # Return NULL invisibly after saving |
195 | 278 | return(invisible(NULL)) |
196 | 279 | } |
197 | 280 | } |
0 commit comments