purrr lift() in R: Change Function Argument Style
The purrr lift() function in R adapts a function so it accepts its arguments in a different shape: as a single list, a single vector, or individual dots. It is a thin wrapper around do.call() for functional pipelines.
lift(sum)(list(1, 2, 3)) # alias of lift_dl lift_dl(sum)(list(1, 2, 3)) # dots-fn -> takes a list lift_dv(paste)(c("a", "b", "c")) # dots-fn -> takes a vector lift_ld(concat)(1, 2, 3) # list-fn -> takes dots lift_vd(g_vec)(1, 2, 3) # vector-fn -> takes dots map_dbl(params, lift_dl(sum)) # apply across a list of arg-lists do.call(sum, list(1, 2, 3)) # base R equivalent
Need explanation? Read on for examples and pitfalls.
What purrr lift() does
lift() converts the argument interface of a function. Some R functions take their inputs as loose dots, like sum(1, 2, 3). Others want a single list or vector, like mean(c(1, 2, 3)). The lift() family rewrites a function so the calling style matches whatever your data already looks like.
This matters inside functional pipelines. When you carry parameters around as lists, a dots-based function will not accept them directly. lift() bridges that gap so the function slots into map(), reduce(), or a %>% chain without a manual wrapper.
do.call() instead. The examples below explain the behaviour you will still meet in older codebases and show the modern replacement.The lift() function family and syntax
Each lift variant is named for a direction. The pattern is lift_<from><to>, where d means dots, l means list, and v means vector. lift_dl() takes a function whose original interface is dots and returns one that takes a list.
Every variant returns a new function. You call lift_dl(sum) once to build the adapter, then call that adapter with your data. Reading the suffix tells you the call style before and after: the first letter is what ..f already expects, the second is what the lifted function will expect.
lift_dl lifts a dots function so it reads from a list. lift_ld does the reverse. Get the direction wrong and the adapter calls your function with the wrong argument shape, which usually fails silently rather than erroring.lift() examples by use case
Start with lift_dl(), the most common variant. sum() accepts dots, so wrapping it with lift_dl() produces a function that sums the contents of a list in one call.
lift_dv() does the same job for a vector instead of a list. The difference is visible with paste(), which behaves differently when fed dots versus a single vector.
The plain call vectorises over the input, while lift_dv() spreads each element into a separate argument, so paste() joins them. lift_ld() runs the opposite conversion: it takes a function that wants a list and lets you call it with loose arguments.
The real payoff arrives with map(). When each element of a list is itself a bundle of arguments, a lifted function applies cleanly across all of them.
lift() vs do.call() vs pmap()
lift() is do.call() reshaped for pipelines. do.call() invokes a function once with an argument list. lift() returns a reusable function you can pass around. pmap() is the modern tool when your arguments line up as parallel columns.
| Tool | Returns | Best when |
|---|---|---|
do.call(f, args) |
The result, immediately | You call f once with a known list |
lift_dl(f) |
A new function | You need a reusable adapter |
pmap(list, f) |
A list of results | Arguments are aligned vectors or columns |
lift_dl(sum), write \(args) do.call(sum, args). It is explicit, has no dependency on a deprecated API, and reads clearly to anyone who knows base R.Common pitfalls
Mistake 1: lifting a function that does not take dots. mean() expects a single argument x. Lifting it and passing several values silently drops the extras.
do.call(mean, list(1, 2, 3)) matches 1 to x and ignores the rest, so the result is mean(1). Only lift functions whose signature genuinely accepts ....
Mistake 2: confusing the suffix direction. lift_dl() and lift_ld() are not interchangeable. Pick the variant whose first letter matches the function you already have.
Mistake 3: assuming lift() is current. On purrr 1.0.0 and later, every lift_* call prints a deprecation warning. In a clean codebase, migrate to do.call() or pmap() before the family is removed.
Try it yourself
Try it: Use a lift variant to find the maximum value stored inside a list. Save the result to ex_max.
Click to reveal solution
Explanation: max() accepts dots, so lift_dl() builds an adapter that reads the values from a list and calls max(4, 9, 2, 7).
Related purrr functions
lift() sits among purrr's function adapters. These tools all transform functions rather than data:
partial()pre-fills some arguments of a function and returns a simpler one.compose()chains several functions into a single pipeline function.negate()flips the result of a TRUE/FALSE predicate.pmap()maps a function over parallel lists or data frame columns.exec()calls a function by name with arguments supplied as dots or a list.
FAQ
What does purrr lift() do?
lift() takes a function and returns a new function that accepts its arguments in a different shape. By default it behaves like lift_dl(): a function that originally took loose dots becomes one that takes a single list. Internally it is a wrapper around do.call(), so the lifted function unpacks the list and passes each element as a separate argument to the original function.
Is lift() deprecated in purrr?
Yes. The entire lift_* family was deprecated in purrr 1.0.0, released in late 2022. The functions still work but print a deprecation warning each time you call them. The maintainers recommend replacing them with an explicit anonymous function around do.call(), such as \(args) do.call(f, args), which has the same effect with no deprecated dependency.
What is the difference between lift_dl and lift_dv?
Both lift a dots-based function, but they differ in what the new function accepts. lift_dl() produces a function that takes a single list, while lift_dv() produces one that takes a single vector. Use lift_dl() when your data is a list and lift_dv() when it is an atomic vector. The conversion direction in both cases is dots to a single container.
How is lift() different from do.call()?
do.call() invokes a function immediately with one argument list and returns the result. lift() does not call anything right away. It returns a reusable function that you can name, store, or pass into map() and reduce(). Think of lift() as do.call() packaged as a function operator, suited to functional pipelines rather than one-off calls.
Related Reading
For the broader picture, see the Functional Programming in R guide.