Skip to content

ielbadisy/functionals

Repository files navigation

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.

As of 0.5.1, progress reporting is completion-driven across sequential, multicore, and cluster-backed execution. When pb = TRUE, the bar advances as individual tasks finish rather than at internal chunk boundaries. Rendering is throttled for large workloads so the console keeps a single lightweight status bar instead of redrawing on every task.

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

# from CRAN
install.packages("functionals")

# from github
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

Parallel progress semantics

slow <- function(x) {
  Sys.sleep(c(0.12, 0.24, 0.36, 0.48)[[x]])
  x^2
}

fmap(1:4, slow, ncores = 2, pb = TRUE)
#>                                                                                 [                              ]   0% 0/4 elapsed 00:00                                                                                [=======                       ]  25% 1/4 elapsed 00:00 eta 00:00                                                                                [===============               ]  50% 2/4 elapsed 00:00 eta 00:00                                                                                [======================        ]  75% 3/4 elapsed 00:00 eta 00:00                                                                                [==============================] 100% 4/4 elapsed 00:01 eta 00:00
#> [[1]]
#> [1] 1
#> 
#> [[2]]
#> [1] 4
#> 
#> [[3]]
#> [1] 9
#> 
#> [[4]]
#> [1] 16

The progress bar advances on each completed task, shows elapsed time and ETA, and throttles redraws for large workloads so intermediate counts stay readable without repainting the console on every iteration.

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% 0/3 elapsed 00:00                                                                                floop says: 1 
#>                                                                                 [==========                    ]  33% 1/3 elapsed 00:00 eta 00:00                                                                                floop says: 2 
#>                                                                                 [====================          ]  67% 2/3 elapsed 00:00 eta 00:00                                                                                floop says: 3 
#>                                                                                 [==============================] 100% 3/3 elapsed 00:00 eta 00:00
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

About

Functional programming in R with built-in support for parallelism and progress bars

Topics

Resources

License

Unknown, MIT licenses found

Licenses found

Unknown
LICENSE
MIT
LICENSE.md

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages