skip to content

In R, what do lapply(), sapply() and vapply() each return, and when is vapply() the safer choice?

level: middleimportance: should knowfreq 38%

answer

  1. always a list
  2. simplify when it can
  3. declare the result shape
  4. zero-length input
  5. apply() wants a matrix

basics

~20 s

In R, lapply() always returns a list; sapply() simplifies to a vector or matrix when results line up, else a list; vapply() requires a declared type and length and errors otherwise, making it predictable in functions.

solid answer

~50 s

All three call a function on each element of a list or vector. **`lapply()`** always returns a **list** of the same length. **`sapply()`** tries to **simplify**: to a vector if every result has length 1, to a **matrix** (one column per element) if all results share a length above 1, and otherwise it gives up and returns a list. The same code can therefore return different types depending on the data, and on empty input `sapply()` returns `list()`. **`vapply()`** takes a template, `FUN.VALUE`, such as `numeric(1)`. Every result must match it in type and length or it **errors**, and empty input gives a typed empty vector. Use `vapply()` inside functions and packages where the result type must be guaranteed. `apply()` is different: it works over matrix margins and converts a data frame with mixed column types to a character matrix first.

code

r · 24 lines
r
scores <- list(north = c(3, 5, 4), south = c(2, 4), west = c(5, 5, 4, 3))

sapply(scores, mean)
#> north south  west 
#>  4.00  3.00  4.25 
sapply(scores, range)
#>      north south west
#> [1,]     3     2    3
#> [2,]     5     4    5
class(sapply(scores, function(s) s[s > 3]))
#> [1] "list"

vapply(scores, mean, numeric(1))
#> north south  west 
#>  4.00  3.00  4.25 
try(vapply(scores, function(s) s[s > 3], numeric(1)))
#> Error in vapply(scores, function(s) s[s > 3], numeric(1)) : 
#>   values must be length 1,
#>  but FUN(X[[1]]) result is length 2

class(sapply(list(), mean))
#> [1] "list"
vapply(list(), mean, numeric(1))
#> numeric(0)

go deeper

for a junior

Recall the return shapes: lapply gives a list, sapply simplifies when it can, and vapply needs a declared result type.

for a middle

Explain sapply's three outcomes (vector, matrix, list), the empty-input case, and why apply() on a mixed data frame coerces everything to character.

for a senior

Show how a report broke when one group returned an empty result through sapply(), and replace it with vapply() or a vectorised expression.

for a principal

Decide conventions for iteration in shared code: when vapply() is required, when a vectorised rewrite pays off, and when a list-returning Map() is the honest shape.

## The shared idea The **apply family** runs a function over pieces of a data structure and collects the results. The members differ mainly in **what they give back**. That is what interviewers probe, because the return shape decides whether the next line of code works. ```r scores <- list(north = c(3, 5, 4), south = c(2, 4), west = c(5, 5, 4, 3)) ``` ## lapply: always a list `lapply(X, FUN)` returns a **list** with one element per element of `X`, keeping names: ```r lapply(scores, mean) # $north 4 $south 3 $west 4.25 ``` It is predictable, but you often want a vector at the end. ## sapply: simplify if you can `sapply()` calls `lapply()` and then tries to simplify the result: | Every result is… | `sapply()` returns | |---|---| | length 1 | a vector, e.g. `sapply(scores, mean)` gives `north 4, south 3, west 4.25` | | the same length k > 1 | a **k x n matrix**, one column per element: `sapply(scores, range)` is 2 x 3 | | of differing lengths | a **list**: `sapply(scores, function(s) s[s > 3])` | | (input has length 0) | `list()` | The data decides the type. A function written against the length-1 case, `result[["north"]] > 3` or `sum(result)`, can break when one group suddenly returns two values or none. That is the classic `sapply()` bug: fine in testing, a list or matrix in production. ## vapply: declare the shape `vapply(X, FUN, FUN.VALUE)` makes you declare what each result looks like: ```r vapply(scores, mean, numeric(1)) # named numeric vector vapply(scores, range, numeric(2)) # 2 x 3 matrix vapply(scores, function(s) s[s > 3], numeric(1)) # Error: values must be length 1, but FUN(X[[1]]) result is length 2 ``` What you gain: 1. **Type safety.** A result of the wrong length or an incompatible type is an **error at the point of the mistake**, not a strange object three lines later. 2. **Stable empty case.** `vapply(list(), mean, numeric(1))` returns `numeric(0)`, not `list()`. 3. **Self-documenting.** The template says what the function is expected to produce. The trade-off is a little more typing. At the console `sapply()` is fine. In a function, a package or a scheduled report, prefer `vapply()`. ## The rest of the family | Function | Iterates over | Returns | |---|---|---| | `apply(X, MARGIN, FUN)` | rows (`1`) or columns (`2`) of a **matrix** or array | vector, matrix or list, simplified like `sapply()` | | `mapply(FUN, ...)` | several vectors **in parallel** | simplified like `sapply()`; `Map()` is the list-returning form | | `tapply(X, INDEX, FUN)` | groups of `X` defined by a factor | an array indexed by group | `apply()` on a **data frame** is the most common trap. It first converts the data frame with `as.matrix()`. A matrix holds one type, so a character column turns **every value into character**: ```r sales <- data.frame(region = c("North", "South"), revenue = c(120, 80)) apply(sales, 1, function(r) r["revenue"] * 2) # Error: non-numeric argument to binary operator ``` For row-wise work on a data frame, vectorise instead (`sales$revenue * 2`). If you really need to iterate, use `mapply()`/`Map()` over the columns you need. ## None of these is vectorisation Every member calls your R function once per element. They replace the bookkeeping of a `for` loop, not its per-element cost. The speed win comes from truly vectorised functions such as `rowSums()` or arithmetic on whole columns.

  • In R, why does apply(df, 1, f) on a data frame with a text column break arithmetic in f?
    `apply()` converts the data frame to a matrix with `as.matrix()`, and a matrix holds a single type. With one character column, every value becomes character, so `r["revenue"] * 2` fails with *non-numeric argument to binary operator*. Vectorise on the column instead, or iterate over just the numeric columns with `mapply()`.
  • In R, how does mapply() differ from sapply()?
    `sapply()` iterates over one vector or list. `mapply()` walks several vectors in parallel, passing their i-th elements together to the function: `mapply(function(p, q) p * q, price, qty)`. It simplifies its result like `sapply()`. `Map()` does the same but always returns a list.

saying these in an interview costs you the question

  • sapply() always returns a vector.
  • The apply family is vectorised and much faster than a for loop.
  • apply() works column by column on a data frame without changing types.
  • vapply() is just sapply() with a different argument order.
  • lapply() simplifies its result when every element has length one.