Learn R Programming

functionals: Functional mapping with parallelism and progress bars

Overview

functionals is a lightweight toolkit for functional programming in R with built-in support for parallelism and progress bars. It extends base R’s functional tools with a consistent, minimal API for mapping, walking, reducing, cross-validating, and repeating computations across lists, data frames, and grouped data.

Function Reference Table

FunctionMain argumentsOutput typeDescription
fmap().x, .f, ncores, pblistMap .f over elements of .x
fmapn().l, .f, ncores, pblistMap .f over multiple aligned lists
fmapr().df, .f, ncores, pblistMap .f over each row of a data frame (as named list)
fmapc().df, .f, ncores, pblistMap .f(column, name) over each column
fmapg().df, .f, by, ncores, pblistMap .f(group_df) over groups defined by a column
floop().x, .f, ..., ncores, pblistGeneral-purpose functional loop with side-effects
fwalk().x, .f, ncores, pbNULLMap .f over .x for side-effects only (invisible return)
frepeat()times, expr, .x, ncores, pblist/vectorRepeat a call/expression multiple times
fcv().splits, .f, ncores, pblistMap .f over resampling splits from rsample::vfold_cv()
freduce().x, .f, ...scalar/listReduce .x using a binary function .f
fcompose()any number of functions f1, f2, ...functionCompose multiple functions: f1(f2(...(x)))
fapply().x, .f, ncores, pb, ...listCore internal utility for applying a function over .x

Syntax Equivalence

Taskfunctionals Examplepurrr ExampleBase R
Map squarefmap(1:5, function(x) x^2)map(1:5, function(x) x^2)lapply(1:5, function(x) x^2)
Map over N argumentsfmapn(list(1:3, 4:6, 7:9), function(x, y, z) x + y + z)pmap(list(1:3, 4:6, 7:9), function(x, y, z) ...)Map(function(x, y, z) ..., 1:3, 4:6, 7:9)
Map over data frame rowsfmapr(df, function(row) row$a + row$b)pmap(df[c("a", "b")], function(x, y) x + y)apply(df, 1, function(row) ...)
Map over data frame colsfmapc(df, function(x, name) mean(x))imap(df, function(x, name) mean(x))lapply(df, mean)
Grouped mapfmapg(df, f, by = "group")map(split(df, df$group), f)lapply(split(df, df$group), f)
General-purpose loopfloop(1:3, function(x) cat(x))(manual recursion)for (x in 1:3) cat(x)
Parallel + progressfmap(x, f, ncores = 4, pb = TRUE)(future_map(x, f)) with progressrparLapply(cl, x, f) or mclapply()
Repeat simulationfrepeat(100, function() rnorm(1))(manual loop)replicate(100, rnorm(1))
Walk with side effectsfwalk(letters, function(x) cat(x))walk(letters, function(x) cat(x))lapply(letters, cat)
Reducefreduce(1:5, `+`)reduce(1:5, `+`)Reduce(`+`, 1:5)
Compose functionsfcompose(sqrt, abs)(-4)compose(sqrt, abs)(-4)(function(x) sqrt(abs(x)))(-4)

Why no formula interface like ~ .x + .y?

While functionals draws inspiration from purrr, it intentionally avoids supporting the formula-based anonymous function syntax (e.g., ~ .x + 1) for now.

This decision is based on:

  • Keeping dependencies minimal (no reliance on rlang)
  • Avoiding non-standard evaluation that can confuse new users
  • Encouraging explicit, readable code using function(x) { ... } style

We may consider adding tidy evaluation support (e.g., with quosures or rlang::as_function) in a future release. However, the current philosophy favors clarity and simplicity.

Installation

# install.packages("functionals") # when available
#remotes::install_github("ielbadisy/functionals")

Examples

library(functionals)
library(purrr)
library(furrr)
#> Loading required package: future
library(pbapply)
library(dplyr)
#> 
#> Attaching package: 'dplyr'
#> The following objects are masked from 'package:stats':
#> 
#>     filter, lag
#> The following objects are masked from 'package:base':
#> 
#>     intersect, setdiff, setequal, union
library(rsample)
library(bench)


plan(multisession)

# utility to compare results
compare_outputs <- function(label, x, y) {
  cat("\n", label, "->", if (identical(x, y)) "dentical\n" else if (isTRUE(all.equal(x, y))) "nearly equal\n" else "different\n")
}

# strip names and convert to plain numeric vector
as_vec <- function(x) as.numeric(unlist(x, use.names = FALSE))

Element-wise map

x1 <- fmap(1:5, function(x) x^2)
x2 <- lapply(1:5, function(x) x^2)
x3 <- map(1:5, ~ .x^2)
x4 <- future_map(1:5, ~ .x^2)
x5 <- pblapply(1:5, function(x) x^2)
compare_outputs("Element-wise: base", x1, x2)
#> 
#>  Element-wise: base -> dentical
compare_outputs("Element-wise: purrr", x1, x3)
#> 
#>  Element-wise: purrr -> dentical
compare_outputs("Element-wise: furrr", x1, x4)
#> 
#>  Element-wise: furrr -> dentical
compare_outputs("Element-wise: pbapply", x1, x5)
#> 
#>  Element-wise: pbapply -> dentical

Multi-input map

x1 <- fmapn(list(1:3, 4:6), function(x, y) x + y)
x2 <- Map(`+`, 1:3, 4:6)
x3 <- pmap(list(1:3, 4:6), ~ ..1 + ..2)
x4 <- future_pmap(list(1:3, 4:6), ~ ..1 + ..2)
compare_outputs("Multi-input: base", x1, x2)
#> 
#>  Multi-input: base -> dentical
compare_outputs("Multi-input: purrr", x1, x3)
#> 
#>  Multi-input: purrr -> dentical
compare_outputs("Multi-input: furrr", x1, x4)
#> 
#>  Multi-input: furrr -> dentical

Row-wise map

x1 <- fmapr(mtcars, function(row) row$mpg + row$cyl)
rowlist <- lapply(seq_len(nrow(mtcars)), function(i) as.list(mtcars[i, ]))
x2 <- lapply(rowlist, function(row) row$mpg + row$cyl)
x3 <- map(rowlist, function(row) row$mpg + row$cyl)
compare_outputs("Row-wise: base", as_vec(x1), as_vec(x2))
#> 
#>  Row-wise: base -> dentical
compare_outputs("Row-wise: purrr", as_vec(x1), as_vec(x3))
#> 
#>  Row-wise: purrr -> dentical

Column-wise map

x1 <- fmapc(mtcars, function(col, name) mean(col))
x2 <- sapply(mtcars, mean)
x3 <- imap(mtcars, ~ mean(.x))
x4 <- future_imap(mtcars, ~ mean(.x))
compare_outputs("Column-wise: base", x1, as.list(x2))
#> 
#>  Column-wise: base -> dentical
compare_outputs("Column-wise: purrr", x1, x3)
#> 
#>  Column-wise: purrr -> dentical
compare_outputs("Column-wise: furrr", x1, x4)
#> 
#>  Column-wise: furrr -> dentical

Group-wise map

x1 <- fmapg(iris, function(df) colMeans(df[1:4]), by = "Species")
x2 <- lapply(split(iris, iris$Species), function(df) colMeans(df[1:4]))
x3 <- map(split(iris, iris$Species), ~ colMeans(.x[1:4]))
x4 <- future_map(split(iris, iris$Species), ~ colMeans(.x[1:4]))
compare_outputs("Group-wise: base", x1, x2)
#> 
#>  Group-wise: base -> dentical
compare_outputs("Group-wise: purrr", x1, x3)
#> 
#>  Group-wise: purrr -> dentical
compare_outputs("Group-wise: furrr", x1, x4)
#> 
#>  Group-wise: furrr -> dentical

Side-effect map

cat("\nSide-effects:\n")
#> 
#> Side-effects:
fwalk(1:3, print)
#> [1] 1
#> [1] 2
#> [1] 3

General-purpose loop with return values

x1 <- floop(1:5, function(x) x^2, .capture = TRUE)
x2 <- lapply(1:5, function(x) x^2)
x3 <- {
  out <- list()
  for (i in 1:5) out[[i]] <- i^2
  out
}
compare_outputs("floop() vs lapply()", x1, x2)
#> 
#>  floop() vs lapply() -> dentical
compare_outputs("floop() vs for()", x1, x3)
#> 
#>  floop() vs for() -> dentical

General-purpose loop (side-effect only)

cat("\nGeneral-purpose loop (side-effects):\n")
#> 
#> General-purpose loop (side-effects):
floop(1:3, function(x) cat("floop says:", x, "\n"), pb = TRUE, .capture = FALSE)
#>  |                                                  |   0% elapsed=00h 00m 00s, remaining~...floop says: 1 
#>  |================                                  |  33% elapsed=00h 00m 00s, remaining~00h 00m 00sfloop says: 2 
#>  |=================================                 |  67% elapsed=00h 00m 00s, remaining~00h 00m 00sfloop says: 3 
#>  |==================================================| 100% elapsed=00h 00m 00s, remaining~00h 00m 00s
cat("for-loop equivalent:\n")
#> for-loop equivalent:
for (x in 1:3) cat("for says:", x, "\n")
#> for says: 1 
#> for says: 2 
#> for says: 3

Cross-validation

splits <- vfold_cv(iris, v = 3)$splits
fit_model <- function(split) mean(analysis(split)$Sepal.Length)
x1 <- fcv(splits, fit_model)
x2 <- lapply(splits, fit_model)
compare_outputs("CV map: base", x1, x2)
#> 
#>  CV map: base -> dentical

Repeat simulation

x1 <- frepeat(times = 10, expr = rnorm(1))
x2 <- as.list(replicate(10, rnorm(1)))
x3 <- as.list(pbreplicate(10, rnorm(1)))
cat("\nRepeat: Results not comparable (randomized output)\n")
#> 
#> Repeat: Results not comparable (randomized output)

Reduce

x1 <- freduce(1:5, `+`)
x2 <- Reduce(`+`, 1:5)
x3 <- reduce(1:5, `+`)
compare_outputs("Reduce: base", x1, x2)
#> 
#>  Reduce: base -> dentical
compare_outputs("Reduce: purrr", x1, x3)
#> 
#>  Reduce: purrr -> dentical

Compose

x1 <- fcompose(sqrt, abs)(-4)
x2 <- (function(x) sqrt(abs(x)))(-4)
x3 <- compose(sqrt, abs)(-4)
compare_outputs("Compose: base", x1, x2)
#> 
#>  Compose: base -> dentical
compare_outputs("Compose: purrr", x1, x3)
#> 
#>  Compose: purrr -> dentical

Copy Link

Version

Install

install.packages('functionals')

Version

0.5.0

License

MIT + file LICENSE

Maintainer

Imad El Badisy

Last Published

July 18th, 2025

Functions in functionals (0.5.0)

fmapn

Apply a function over multiple argument lists in parallel
freduce

Functional reduce
fmap

Functional mapping with optional parallelism and progress bars
fcv

Functional Cross-Validation mapping
fcompose

Compose multiple functions
floop

Functional loop with optional parallelism and progress bar
fmapr

Apply a function row-wise on a data frame with parallelism
fapply

Apply a function over a list or vector with optional parallelism and progress
fmapg

Apply a function to groups of a data frame in parallel
fmapc

Apply a function column-wise with name access and parallelism
fwalk

Walk over a vector or list with side effects
frepeat

Repeat an expression or function call multiple times
functionals-package

functionals: Functional programming with parallelism and progress track in R