-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathREADME.Rmd
More file actions
270 lines (196 loc) · 10 KB
/
Copy pathREADME.Rmd
File metadata and controls
270 lines (196 loc) · 10 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
---
output: github_document
---
```{r, include = FALSE}
knitr::opts_chunk$set(
collapse = TRUE,
comment = "#>",
fig.path = "man/figures/README-",
out.width = "100%"
)
```
# 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
```{r, eval=FALSE}
# from CRAN
install.packages("functionals")
# from github
remotes::install_github("ielbadisy/functionals")
```
## Examples
```{r}
library(functionals)
library(purrr)
library(furrr)
library(pbapply)
library(dplyr)
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
```{r}
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)
compare_outputs("Element-wise: purrr", x1, x3)
compare_outputs("Element-wise: furrr", x1, x4)
compare_outputs("Element-wise: pbapply", x1, x5)
```
### Parallel progress semantics
```{r}
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)
```
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
```{r}
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)
compare_outputs("Multi-input: purrr", x1, x3)
compare_outputs("Multi-input: furrr", x1, x4)
```
### Row-wise map
```{r}
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))
compare_outputs("Row-wise: purrr", as_vec(x1), as_vec(x3))
```
### Column-wise map
```{r}
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))
compare_outputs("Column-wise: purrr", x1, x3)
compare_outputs("Column-wise: furrr", x1, x4)
```
### Group-wise map
```{r}
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)
compare_outputs("Group-wise: purrr", x1, x3)
compare_outputs("Group-wise: furrr", x1, x4)
```
### Side-effect map
```{r}
cat("\nSide-effects:\n")
fwalk(1:3, print)
```
### General-purpose loop with return values
```{r}
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)
compare_outputs("floop() vs for()", x1, x3)
```
### General-purpose loop (side-effect only)
```{r}
cat("\nGeneral-purpose loop (side-effects):\n")
floop(1:3, function(x) cat("floop says:", x, "\n"), pb = TRUE, .capture = FALSE)
cat("for-loop equivalent:\n")
for (x in 1:3) cat("for says:", x, "\n")
```
### Cross-validation
```{r}
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)
```
### Repeat simulation
```{r}
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")
```
### Reduce
```{r}
x1 <- freduce(1:5, `+`)
x2 <- Reduce(`+`, 1:5)
x3 <- reduce(1:5, `+`)
compare_outputs("Reduce: base", x1, x2)
compare_outputs("Reduce: purrr", x1, x3)
```
### Compose
```{r}
x1 <- fcompose(sqrt, abs)(-4)
x2 <- (function(x) sqrt(abs(x)))(-4)
x3 <- compose(sqrt, abs)(-4)
compare_outputs("Compose: base", x1, x2)
compare_outputs("Compose: purrr", x1, x3)
```