Skip to content

Commit 19e3ed4

Browse files
committed
Fix RecordArgs
1 parent 4fa4d32 commit 19e3ed4

File tree

5 files changed

+239
-99
lines changed

5 files changed

+239
-99
lines changed

R/General_RecordArgs.R

Lines changed: 153 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -8,35 +8,51 @@
88
#' unevaluated and evaluated forms of arguments passed to a parent function. It
99
#' returns a tibble with columns reflecting argument states: when unevaluated
1010
#' 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.
1515
#'
1616
#' @param ExportPath Character. The path to an `.RData` file where the tibble
1717
#' will be exported. If `NULL` (default), the tibble is returned without
1818
#' saving. If provided, the tibble is saved to the specified file and `NULL`
1919
#' 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.
2029
#'
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
2944
#' in that order.
3045
#'
3146
#' @return A `tibble` containing the unevaluated and evaluated forms of the
3247
#' parent function’s arguments. Column naming depends on whether unevaluated
3348
#' and evaluated values match:
3449
#' - **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),
3652
#' containing the evaluated value as-is.
3753
#' - **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.
4056
#' - `*_eval`: Evaluated values, either scalars (e.g., `8`) or list columns
4157
#' for complex objects (e.g., `lm`, `SpatRaster`).
4258
#'
@@ -46,152 +62,219 @@
4662
#'
4763
#' @author Ahmed El-Gabbas
4864
#' @export
65+
#' @name RecordArgs
4966
#' @examples
5067
#' a <- 5
5168
#' b <- 3
69+
#'
5270
#' Function1 <- function(w = 5, x, y, z = 10) {
53-
#' Args <- RecordArgs()
71+
#' Args <- IASDT.R::RecordArgs(call = match.call(), env = parent.frame())
5472
#' return(Args)
5573
#' }
5674
#'
5775
#' # --------------------------------------------------------------
5876
#'
5977
#' # Basic usage with scalars and expressions
6078
#' 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)
6687
#'
6788
#' # --------------------------------------------------------------
6889
#'
69-
#' #' # Usage with complex objects (lm and SpatRaster)
90+
#' # Usage with complex objects (lm and Raster)
7091
#' Out2 <- Function1(
7192
#' w = 10,
7293
#' x = a + b,
7394
#' 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+
#'
7599
#' 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)"
79103
#' 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
82106
#'
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) {
83134

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)
87138

139+
# Check if call_info is valid; stop if not called within a function
88140
if (is.null(call_info)) {
89141
stop(
90142
"RecordArgs() must be called from within another function", call. = FALSE)
91143
}
92144

93-
# Extract the arguments, excluding the function name
145+
# Extract the arguments from the call, excluding the function name (first
146+
# element)
94147
args_list <- as.list(call_info)[-1]
95148

96-
# Extract the name of the calling function
149+
# Get the name of the calling function as a character string
97150
calling_func <- deparse(call_info[[1]])
98151

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)
101157
parent_func <- sys.function(-1)
102158
formals_full <- formals(parent_func)
103159

104-
# Evaluate the arguments in the parent environment
160+
# Evaluate the captured arguments in the parent environment
105161
args_values <- lapply(args_list, eval, envir = parent_env)
162+
163+
# Name the evaluated values with their corresponding argument names
106164
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
108168
Evaluated <- utils::modifyList(formals_full, recorded_values)
109-
# Store unevaluated expressions
169+
170+
# Merge unevaluated expressions with defaults, keeping unevaluated forms
110171
Unevaluated <- utils::modifyList(formals_full, args_list)
111172

112-
# Get argument names in their original order
173+
# Get the argument names in their original order from the function definition
113174
arg_names <- names(formals_full)
114175

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
116180
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),
119184
.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
121190
identical(u, e)
122191
})
123192

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
125197
single_cols <- arg_names[same_values]
126-
# Names for orig/eval pairs
198+
# Names for arguments needing _orig/_eval pairs
127199
diff_cols <- arg_names[!same_values]
128200

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
133202

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
135212
eval_values <- purrr::map(
136213
.x = as.list(Evaluated),
137214
.f = function(x) {
138215
if (is.vector(x) && length(x) == 1 && !is.list(x)) {
216+
# Scalars remain as-is (e.g., 8, "BCD")
139217
return(x)
140218
} else {
141219
if (inherits(x, "SpatRaster")) {
220+
# Wrap SpatRaster objects for storage
142221
x <- terra::wrap(x)
143222
}
223+
# Wrap complex objects (e.g., lm, RasterLayer) in a list
144224
return(list(x))
145225
}
146226
})
147227

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
150230
uneval_values <- purrr::map(
151231
.x = as.list(Unevaluated),
152232
.f = function(x) {
153233
if (is.call(x)) {
154-
return(list(x))
234+
# Convert calls (e.g., a + b) to strings without quotes
235+
return(noquote(deparse(x)))
155236
} else if (is.vector(x) && length(x) == 1 && !is.list(x)) {
237+
# Scalars (e.g., 2, 10) remain as-is
156238
return(x)
157239
} else {
158240
if (inherits(x, "SpatRaster")) {
241+
# Wrap SpatRaster objects
159242
x <- terra::wrap(x)
160243
}
244+
# Wrap complex objects in a list
161245
return(list(x))
162246
}
163247
})
164248

165-
# Combine into a tibble
249+
# Combine all values into a named list for tibble construction
166250
tibble_data <- c(
251+
# Unevaluated differing values
167252
stats::setNames(uneval_values[diff_cols], uneval_cols),
253+
# Evaluated differing values
168254
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))
171257

172-
# Create tibble and reorder columns by argument order
258+
# Create the tibble from the combined data
173259
result <- tibble::as_tibble(tibble_data)
174260

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))
187267

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
189269
if (is.null(ExportPath)) {
270+
# Return the tibble if no export path is provided
190271
return(result)
191272
} else {
273+
# Save to .RData file if ExportPath is specified
192274
IASDT.R::SaveAs(
193275
InObj = result, OutObj = paste0("Args_", calling_func),
194276
OutPath = ExportPath)
277+
# Return NULL invisibly after saving
195278
return(invisible(NULL))
196279
}
197280
}

R/Mod_Postprocess.R

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,7 @@ Mod_Postprocess_1_CPU <- function(
208208
rm(EnvVars2Read, envir = environment())
209209

210210
IASDT.R::RecordArgs(
211+
call = match.call(), env = parent.frame(),
211212
ExportPath = IASDT.R::Path(Path_Model, "Args_Mod_Postprocess_1_CPU.RData"))
212213

213214
# ****************************************************************
@@ -455,6 +456,7 @@ Mod_Prep_TF <- function(
455456
rm(EnvVars2Read, envir = environment())
456457

457458
IASDT.R::RecordArgs(
459+
call = match.call(), env = parent.frame(),
458460
ExportPath = IASDT.R::Path(Path_Model, "Args_Mod_Prep_TF.RData"))
459461

460462
# ****************************************************************
@@ -872,6 +874,7 @@ Mod_Postprocess_2_CPU <- function(
872874
rm(EnvVars2Read, envir = environment())
873875

874876
IASDT.R::RecordArgs(
877+
call = match.call(), env = parent.frame(),
875878
ExportPath = IASDT.R::Path(Path_Model, "Args_Mod_Postprocess_2_CPU.RData"))
876879

877880
# ****************************************************************

R/Mod_Prep4HPC.R

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -312,6 +312,7 @@ Mod_Prep4HPC <- function(
312312
}
313313

314314
IASDT.R::RecordArgs(
315+
call = match.call(), env = parent.frame(),
315316
ExportPath = IASDT.R::Path(Path_Model, "Args_Prep4HPC.RData"))
316317

317318
# # ..................................................................... ###

R/Mod_PrepData.R

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ Mod_PrepData <- function(
102102
fs::dir_create(Path_Model)
103103

104104
IASDT.R::RecordArgs(
105+
call = match.call(), env = parent.frame(),
105106
ExportPath = IASDT.R::Path(Path_Model, "Args_PrepData.RData"))
106107

107108
# # ..................................................................... ###

0 commit comments

Comments
 (0)