R Programming / Language and data foundations / Lesson 5
Functions, lexical scope, and validation
Make arguments, captured state, return values, and failure conditions part of a reusable function contract.
On this page
Lesson goals
- Trace name lookup through a function’s enclosing environment.
- Distinguish vectorized logic from scalar control flow.
- Validate arguments and expose meaningful failures.
A function has arguments, a body, and an environment
A function is an object that can be assigned, passed to another function, and returned. Its enclosing environment participates in lexical name lookup: a free name is resolved using the environment in which the function was created, rather than simply the caller’s local variables. This makes closures useful for configured calculations, but can also hide dependencies if the enclosing state changes. Prefer explicit arguments for ordinary data inputs. Document any captured configuration and avoid using global assignment as a shortcut for returning a result. A pure-looking function name does not establish absence of side effects.
A closure can retain a deliberate configuration
The first fixture creates a function that adds a retained offset. The outer call supplies three, and the returned closure keeps access to that value even though a separate global offset is later set to 100. R arguments use lazy evaluation, so force makes the intended capture time explicit here. This is especially helpful when building functions from expressions whose referenced state might later change. The example demonstrates one closure, not a full treatment of promises or evaluation metaprogramming. Later advanced lessons inspect those mechanisms with a more explicit environment model.
make_shift <- function(offset) {
force(offset)
function(x) x + offset
}
shift_three <- make_shift(3)
offset <- 100
cat("shifted=", shift_three(2), "\n", sep = "")
cat("global offset=", offset, "\n", sep = "")
shifted=5
global offset=100
The closure retains its creation environment; forcing the supplied offset makes capture timing explicit in this example.
Scalar decisions differ from elementwise masks
An if condition must resolve to one nonmissing logical decision. Comparisons of vectors usually return vectors, and & or | combine them elementwise. The short-circuit operators && and || are for scalar control decisions and can avoid evaluating an inappropriate later check. The second fixture first checks numeric type, then nonempty length, then finiteness. all reduces a logical vector to one decision, but all(logical(0)) is true by its identity convention, so the separate length check matters. A missing condition should be handled explicitly rather than treated as an ordinary false result.
Validation should explain the caller’s violation
The checked summary rejects unsupported input with a stable custom message before computing its result. A caller can handle that error through tryCatch, as the fixture shows. Do not catch every error and return zero, because zero is a plausible measurement that hides failure. Decide which failures are expected input cases and which indicate a programming defect that should remain visible. Validate only requirements the function actually needs: finite numeric input is a technical condition, while nonnegative durations or a particular unit require an additional domain contract. Keep those meanings clear in the function’s documentation.
checked_mean <- function(x) {
if (!is.numeric(x) || length(x) == 0L || !all(is.finite(x))) {
stop("expected nonempty finite numeric values")
}
mean(x)
}
cat("mean=", checked_mean(c(2, 6)), "\n", sep = "")
message <- tryCatch(checked_mean(numeric(0)), error = function(e) conditionMessage(e))
cat(message, "\n", sep = "")
mean=4
expected nonempty finite numeric values
Scalar short-circuit checks validate the argument before evaluating the mean; the custom error text is intentionally stable.
Return values support composition and testing
R returns the last evaluated expression unless return is used explicitly. Named list or vector results make downstream interpretation clearer than assigning several objects into a caller’s environment. Avoid printing as the only way to communicate a value that later code must use. Test ordinary input, invalid type, empty input, missingness, and any relevant boundary. Tests should examine values, type, names, and intended errors, not merely whether an expression completed. The provided R programs remain illustrative because no R runtime was available; their output and custom-error paths are reasoned expectations, not claimed execution evidence.
Common errors and fixes
A function behaves differently after an unrelated global assignment
- Why it happens
- A free name resolved to mutable enclosing state.
- How to fix it
- Pass the value explicitly or capture deliberate configuration.
- Prevent it next time
- Review free names and test independence from irrelevant globals.
An if statement receives several logical values
- Why it happens
- A vector comparison was used as a scalar decision.
- How to fix it
- Use a justified reduction such as all or any, or vectorized selection.
- Prevent it next time
- Specify whether the contract concerns every element or at least one.
Empty input passes a finiteness check
- Why it happens
- all on an empty logical vector returns its identity value.
- How to fix it
- Check required length separately before computing a summary.
- Prevent it next time
- Include empty vectors in argument-contract tests.
Check your understanding
1. Why does the closure return five after the global offset becomes 100?
Show worked solution
The returned function resolves offset in its retained creation environment, where the supplied value was forced to three. It adds that retained three to two. The global variable is a different binding. A closure that deliberately referred to another mutable environment would require a different contract.
2. Why is returning zero after any error unsafe for a mean function?
Show worked solution
Zero can be a legitimate average and gives callers no way to distinguish data from failure. Catch only expected conditions when the caller has a defined response, preserve an error or structured status otherwise, and ensure an invalid input cannot silently become a valid-looking measurement.
Key takeaways
- Lexical environments explain captured state.
- Scalar control flow and vectorized masks have different roles.
- Explicit values and explicit failures make functions composable.