README.md

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

| Function | Main arguments | Output type | Description | |--------------|---------------------------------------|-------------|-------------------------------------------------------------| | fmap() | .x, .f, ncores, pb | list | Map .f over elements of .x | | fmapn() | .l, .f, ncores, pb | list | Map .f over multiple aligned lists | | fmapr() | .df, .f, ncores, pb | list | Map .f over each row of a data frame (as named list) | | fmapc() | .df, .f, ncores, pb | list | Map .f(column, name) over each column | | fmapg() | .df, .f, by, ncores, pb | list | Map .f(group_df) over groups defined by a column | | floop() | .x, .f, ..., ncores, pb | list | General-purpose functional loop with side-effects | | fwalk() | .x, .f, ncores, pb | NULL | Map .f over .x for side-effects only (invisible return) | | frepeat() | times, expr, .x, ncores, pb | list/vector | Repeat a call/expression multiple times | | fcv() | .splits, .f, ncores, pb | list | Map .f over resampling splits from rsample::vfold_cv() | | freduce() | .x, .f, ... | scalar/list | Reduce .x using a binary function .f | | fcompose() | any number of functions f1, f2, ... | function | Compose multiple functions: f1(f2(...(x))) | | fapply() | .x, .f, ncores, pb, ... | list | Core internal utility for applying a function over .x |

Syntax Equivalence

| Task | functionals Example | purrr Example | Base R | |--------------------------|-----------------------------------------------------------|----------------------------------------------------|---------------------------------------------| | Map square | fmap(1:5, function(x) x^2) | map(1:5, function(x) x^2) | lapply(1:5, function(x) x^2) | | Map over N arguments | fmapn(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 rows | fmapr(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 cols | fmapc(df, function(x, name) mean(x)) | imap(df, function(x, name) mean(x)) | lapply(df, mean) | | Grouped map | fmapg(df, f, by = "group") | map(split(df, df$group), f) | lapply(split(df, df$group), f) | | General-purpose loop | floop(1:3, function(x) cat(x)) | (manual recursion) | for (x in 1:3) cat(x) | | Parallel + progress | fmap(x, f, ncores = 4, pb = TRUE) | (future_map(x, f)) with progressr | parLapply(cl, x, f) or mclapply() | | Repeat simulation | frepeat(100, function() rnorm(1)) | (manual loop) | replicate(100, rnorm(1)) | | Walk with side effects | fwalk(letters, function(x) cat(x)) | walk(letters, function(x) cat(x)) | lapply(letters, cat) | | Reduce | freduce(1:5, `+`) | reduce(1:5, `+`) | Reduce(`+`, 1:5) | | Compose functions | fcompose(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:

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


Try the functionals package in your browser

Any scripts or data that you put into this service are public.

functionals documentation built on Aug. 8, 2025, 7:32 p.m.