library(tidyverse)
## ── Attaching packages ──────────────────────────────── tidyverse 1.3.0.9000 ──
## ✓ ggplot2 3.3.0            ✓ purrr   0.3.3.9000  
## ✓ tibble  2.99.99.9014     ✓ dplyr   0.8.5       
## ✓ tidyr   1.0.2            ✓ stringr 1.4.0       
## ✓ readr   1.3.1.9000       ✓ forcats 0.5.0
## ── Conflicts ──────────────────────────────────────── tidyverse_conflicts() ──
## x dplyr::filter() masks stats::filter()
## x dplyr::lag()    masks stats::lag()
library(vctrs)
library(rlang)
## 
## Attaching package: 'rlang'
## The following objects are masked from 'package:purrr':
## 
##     %@%, as_function, flatten, flatten_chr, flatten_dbl, flatten_int,
##     flatten_lgl, flatten_raw, invoke, list_along, modify, prepend,
##     splice

Implementation sketch

fast_if_else <- function(condition, true, false, ..., .env = caller_env()) {
  dots <- enquos(..., .named = TRUE)
  if (is_empty(dots) && exists(".data", .env, inherits = FALSE)) {
    # FIXME: Install lazy data mask
    dots <- as.list(.env$.top_env)
  } else {
    dots <- lapply(dots, eval_tidy)
  }

  data <- tibble(!!!dots)

  true <- enexpr(true)
  false <- enexpr(false)
  conds <- list(false, true)

  grouped_data <-
    data %>%
    group_by(..idx = !!condition + 1L)

  rows <-
    grouped_data %>%
    group_rows()

  recipe <-
    grouped_data %>%
    summarize(
      res = list(eval_tidy(conds[[ ..idx[[1]] ]])),
      target_idx = rows[ ..idx[[1]] ]
    ) %>%
    ungroup()

  target_idx <- recipe$target_idx
  res <- recipe$res

  out_idx <- rep(NA_integer_, length(condition))
  out_idx[ target_idx[[1]] ] <- seq_along(target_idx[[1]])
  out <- vec_slice(res[[1]], out_idx)

  for (i in seq2(2, nrow(recipe))) {
    out <- vec_assign(out, target_idx[[i]], res[[i]])
  }

  out
}

slow <- function(x) {
  Sys.sleep(0.05 * length(x))
  x
}

Results

x <- 1:10

system.time(
  if_else(x < 5, slow(x), slow(-x), x)
)
##    user  system elapsed 
##   0.008   0.000   1.010
system.time(
  fast_if_else(x < 5, slow(x), slow(-x), x)
)
##    user  system elapsed 
##   0.030   0.000   0.531
tibble(xx = x) %>%
  mutate(yy = fast_if_else(xx < 5, slow(xx), slow(-xx)))
## # A tibble: 10 x 2
##       xx    yy
##    <int> <int>
##  1     1     1
##  2     2     2
##  3     3     3
##  4     4     4
##  5     5    -5
##  6     6    -6
##  7     7    -7
##  8     8    -8
##  9     9    -9
## 10    10   -10