Description
Functional Programming with Parallelism and Progress Tracking.
Description
Provides functional tools such as fmap(), fwalk(), and fapply() to iterate over vectors, data frames, or grouped data with optional parallelism and real-time progress tracking. Designed for readable and reproducible workflows, including support for Monte Carlo simulations and benchmarking.
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:
- 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