|
| 1 | +#' Capture and record function arguments |
| 2 | +#' |
| 3 | +#' `RecordArgs()` is a utility function designed to capture and record both the |
| 4 | +#' evaluated and unevaluated forms of arguments passed to a parent function. It |
| 5 | +#' returns a `tibble` where each argument is represented by two columns: one for |
| 6 | +#' the unevaluated expression (suffix `_orig`) and one for the evaluated value |
| 7 | +#' (suffix `_eval`). The function dynamically handles scalars, call objects, and |
| 8 | +#' complex objects (e.g., `lm` models, `SpatRaster` objects), preserving their |
| 9 | +#' structure appropriately. |
| 10 | +#' @return A tibble containing the unevaluated and evaluated forms of the parent |
| 11 | +#' function’s arguments, with columns named using the argument names followed |
| 12 | +#' by `_orig` and `_eval` suffixes. if `ExportPath` is `NULL` (default), the |
| 13 | +#' tibble is returned; otherwise, it is saved as an `.RData` file and returns |
| 14 | +#' `NULL`. |
| 15 | +#' @author Ahmed El-Gabbas |
| 16 | +#' @name RecordArgs |
| 17 | +#' @export |
| 18 | +#' @param ExportPath Character. The path of an `RData` file to export the tibble |
| 19 | +#' to . If `NULL` (default), the tibble will not be saved to disk, but will be |
| 20 | +#' returned as an object. |
| 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 converted to character strings, while evaluated values are kept as-is |
| 26 | +#' for scalars or wrapped in lists for complex objects. Columns are ordered |
| 27 | +#' based on the original argument sequence in the parent function’s |
| 28 | +#' definition, with unevaluated variants preceding evaluated variants (e.g., |
| 29 | +#' `x_orig`, `x_eval`). |
| 30 | +#' @examples |
| 31 | +#' # Basic usage with scalars and expressions |
| 32 | +#' a <- 5 |
| 33 | +#' b <- 3 |
| 34 | +#' Fun1 <- function(w = 5, x, y, z = 10) { |
| 35 | +#' Args <- RecordArgs() |
| 36 | +#' return(Args) |
| 37 | +#' } |
| 38 | +#' AA <- Fun1(x = a + b, y = 2) |
| 39 | +#' AA$w_orig # 5 |
| 40 | +#' AA$w_eval # 5 |
| 41 | +#' AA$x_orig # "a + b" |
| 42 | +#' AA$x_eval # 8 |
| 43 | +#' AA$y_orig # 2 |
| 44 | +#' AA$y_eval # 2 |
| 45 | +#' AA$z_orig # 10 |
| 46 | +#' AA$z_eval # 10 |
| 47 | +#' |
| 48 | +#' # Usage with complex objects (lm and SpatRaster) |
| 49 | +#' AA <- Fun1( |
| 50 | +#' w = 10, |
| 51 | +#' x = a + b, |
| 52 | +#' y = stats::lm(mpg ~ disp + hp, data = mtcars), |
| 53 | +#' z = terra::rast(system.file("ex/logo.tif", package = "terra"))) |
| 54 | +#' AA$w_orig # 10 |
| 55 | +#' AA$w_eval # 10 |
| 56 | +#' AA$x_orig # "a + b" |
| 57 | +#' AA$x_eval # 8 |
| 58 | +#' AA$y_orig # "lm(mpg ~ disp + hp, data = mtcars)" |
| 59 | +#' AA$y_eval[[1]] # lm object |
| 60 | +#' AA$z_orig # "terra::rast(system.file("ex/logo.tif", package = "terra"))" |
| 61 | +#' AA$z_eval[[1]] # SpatRaster object |
| 62 | + |
| 63 | +RecordArgs <- function(ExportPath = NULL) { |
| 64 | + |
| 65 | + # Get the call to the parent function (one level up) |
| 66 | + call_info <- sys.call(-1) |
| 67 | + |
| 68 | + if (is.null(call_info)) { |
| 69 | + stop( |
| 70 | + "RecordArgs() must be called from within another function", |
| 71 | + call. = FALSE) |
| 72 | + } |
| 73 | + |
| 74 | + # Extract the arguments, excluding the function name |
| 75 | + args_list <- as.list(call_info)[-1] |
| 76 | + |
| 77 | + # Extract the name of the calling function |
| 78 | + calling_func <- deparse(call_info[[1]]) |
| 79 | + |
| 80 | + # Get the parent function's environment and formal arguments |
| 81 | + parent_env <- parent.frame() |
| 82 | + parent_func <- sys.function(-1) |
| 83 | + formals_full <- formals(parent_func) |
| 84 | + |
| 85 | + # Initialize results |
| 86 | + Evaluated <- NULL |
| 87 | + Unevaluated <- NULL |
| 88 | + |
| 89 | + # Evaluate the arguments in the parent environment |
| 90 | + args_values <- lapply(args_list, eval, envir = parent_env) |
| 91 | + recorded_values <- stats::setNames(args_values, names(args_list)) |
| 92 | + # Combine with default values |
| 93 | + Evaluated <- modifyList(formals_full, recorded_values) |
| 94 | + # Store unevaluated expressions |
| 95 | + Unevaluated <- modifyList(formals_full, args_list) |
| 96 | + |
| 97 | + # Get argument names in their original order - use formal argument order |
| 98 | + arg_names <- names(formals_full) |
| 99 | + |
| 100 | + # Construct column names |
| 101 | + eval_cols <- paste0(arg_names, "_eval") |
| 102 | + uneval_cols <- paste0(arg_names, "_orig") |
| 103 | + |
| 104 | + # Prepare Evaluated values: keep scalars as-is, wrap complex objects in lists |
| 105 | + eval_values <- purrr::map( |
| 106 | + .x = as.list(Evaluated), |
| 107 | + .f = function(x) { |
| 108 | + if (is.vector(x) && length(x) == 1 && !is.list(x)) { |
| 109 | + # Scalars (numeric, character) stay as-is |
| 110 | + return(x) |
| 111 | + } else { |
| 112 | + # Complex objects (e.g., lm, SpatRaster) wrapped in list |
| 113 | + |
| 114 | + if (inherits(x, "SpatRaster")) { |
| 115 | + x <- terra::wrap(x) |
| 116 | + } |
| 117 | + |
| 118 | + return(list(x)) |
| 119 | + } |
| 120 | + }) |
| 121 | + |
| 122 | + # Prepare Unevaluated values: keep calls as-is, wrap in list, scalars stay |
| 123 | + # as-is |
| 124 | + uneval_values <- purrr::map( |
| 125 | + .x = as.list(Unevaluated), |
| 126 | + .f = function(x) { |
| 127 | + if (is.call(x)) { |
| 128 | + # Keep call objects as-is, wrapped in list |
| 129 | + return(list(x)) |
| 130 | + } else if (is.vector(x) && length(x) == 1 && !is.list(x)) { |
| 131 | + # Scalars stay as-is |
| 132 | + return(x) |
| 133 | + } else { |
| 134 | + # Other objects wrapped in list |
| 135 | + if (inherits(x, "SpatRaster")) { |
| 136 | + x <- terra::wrap(x) |
| 137 | + } |
| 138 | + return(list(x)) |
| 139 | + } |
| 140 | + }) |
| 141 | + |
| 142 | + # Combine into a tibble |
| 143 | + tibble_data <- c( |
| 144 | + # Unevaluated first |
| 145 | + stats::setNames(uneval_values, uneval_cols), |
| 146 | + # Evaluated second |
| 147 | + stats::setNames(eval_values, eval_cols)) |
| 148 | + |
| 149 | + # Create tibble and reorder columns by argument order |
| 150 | + result <- tibble::as_tibble(tibble_data) |
| 151 | + |
| 152 | + # Define the desired column order: unevaluated before evaluated for each arg |
| 153 | + desired_order <- lapply( |
| 154 | + arg_names, |
| 155 | + function(n) { |
| 156 | + c(paste0(n, "_orig"), paste0(n, "_eval")) |
| 157 | + }) %>% |
| 158 | + unlist() |
| 159 | + |
| 160 | + result <- dplyr::select(result, tidyselect::all_of(desired_order)) |
| 161 | + |
| 162 | + # Export the tibble to a file if a path is provided |
| 163 | + if (is.null(ExportPath)) { |
| 164 | + return(result) |
| 165 | + } else { |
| 166 | + IASDT.R::SaveAs( |
| 167 | + InObj = result, OutObj = paste0(calling_func, "_args"), |
| 168 | + OutPath = ExportPath) |
| 169 | + return(invisible(NULL)) |
| 170 | + } |
| 171 | + |
| 172 | + |
| 173 | +} |
0 commit comments