What's New in R 4.4
R 4.4.0, nicknamed “Puppy Cup,” landed on April 24th, 2024. This release is notable for bringing experimental tail-call optimization to base R—a feature that has long been requested by programmers who work with recursive algorithms. Beyond that, there are important changes to how R handles NULL values and complex numbers that may affect existing code.
This article covers the changes that matter most for practical R programming.
Experimental Tail-Call Optimization with Tailcall()
Recursion is elegant for problems like tree traversal, factorial calculation, and algorithms that naturally divide into smaller subproblems. The catch is that each recursive call adds a new frame to the call stack, and deep recursion can cause stack overflow errors.
R 4.4 introduces an experimental feature that addresses this: Tailcall(). This function tells R it can reuse the current stack frame instead of creating a new one when the recursive call is in tail position—meaning it is the very last thing the function does.
# Before R 4.4, this could overflow with large n
factorial <- function(n) {
if (n <= 1) return(1)
n * factorial(n - 1)
}
# With R 4.4 tail-call optimization
factorial_tail <- function(n, acc = 1) {
if (n <= 1) return(acc)
Tailcall(factorial_tail, n - 1, n * acc)
}
The acc parameter accumulates the result as we go, making the recursive call the final operation. With Tailcall(), R can optimize this to reuse the stack frame instead of growing the stack infinitely. This is particularly useful for algorithms that would otherwise require thousands of recursive calls.
This is marked experimental, so the behavior may change. It is also not a drop-in replacement for all recursive code—you need to structure functions with tail position in mind. Not every recursive function can be easily converted to use tail calls.
Changes to NULL Handling
R 4.4 changes how certain operations handle NULL values, and some existing code may behave differently.
The c() function now preserves NULL differently in certain contexts:
# Previously NULL was dropped silently
# Now behavior is more consistent
x <- list(NULL, 1, NULL)
c_vec <- c(NULL = "a", 1) # Named NULL elements are now preserved in some cases
The is.null() check remains the primary way to test for NULL, but be aware that subsetting operations that previously returned NULL for missing elements may now return something different.
If you have code that relies on specific NULL-coercion behavior, test it with R 4.4 before deploying. The changes are subtle but may catch code that made assumptions about how NULL propagates through various operations.
Complex Value Improvements
Working with complex numbers in R gets some welcome improvements:
# Complex arithmetic is now more consistent
z <- 1+2i
Conj(z) # Conjugate works as expected
Mod(z) # Modulus
Arg(z) # Argument in radians
# New behavior with NA in complex operations
z_na <- NA_complex_
z_na + 1 # Now properly propagates NA_complex_ instead of coercean incorrectly
The main improvement is that operations involving complex numbers and NA values now behave more predictably and consistently with other numeric types. This fixes edge cases that previously could produce surprising results.
What You Should Do
If you are upgrading to R 4.4:
-
Test recursive code — If you have functions using recursion, verify they still work. The tail-call optimization is optional but may affect stack behavior in unexpected ways for deeply nested calls.
-
Audit NULL handling — Code that relies on specific NULL-coercion quirks may need updates. Pay attention to code that builds vectors dynamically using
c()with potentially NULL values. -
Check complex number operations — Test any code that mixes complex numbers with NA values. The changes are improvements but may alter the output of existing calculations.
-
Update package dependencies — Make sure your critical packages support R 4.4 before upgrading production environments. Most major packages work, but some older packages may have compatibility issues.
The experimental tail-call feature is the most exciting addition. While it is not enabled by default for all recursive functions, having it available in base R opens up new possibilities for elegant solutions to problems that previously required workarounds or external packages.