Code
library(fuj)October 3, 2026
This minor release of {fuj} presents some new features I’m very excited about and have fun working on. {fuj} is a sort of basic dependency package that I use within some of my own personal projects and within my professional work. These are mostly simple functions that allow me to reduce dependencies for long-term use. In some cases, {fuj} contains functions that are inspired by other packages (as is the case with the first section on here) but may be implemented differently, with some changes in behaviors, limited features, or scope reduced to fit within the available functionality of {base} R.1 This isn’t because I think think I can do it better, or faster – and often times, these are missing features and might be less efficient – but so I can be lazier with maintenance.
vapWe all know (and love) {purrr}’s map() functions. This is a case of reinvention and complexity reduction so I can use some of the same features without the full dependency on {purrr}.
Thus, vap functions. These are vector apply functions, with a few small differences.
For the most part:
{fuj} |
{purrr} |
|---|---|
vap(), vap2(), vapi(), vapp() |
map(), map2(), imap(), pmap() |
vap3() |
(none) |
vap_lgl(), vap_int(), vap_dbl(), vap_chr(), vap_raw() |
map_lgl(), map_int(), map_dbl(), map_chr(), map_raw() |
vap_cpl(), vap_date(), vap_dttm() |
(none) |
And these extend to vap2_*(), vap3_*(), vapi_*(), vapp_*() functions.
All {fuj} functions rely on coercing rather than validating, like the {purrr} functions do. This makes the functions a little more flexible, but at the cost of potentially causing downstream errors. {fuj} presumes that you know what you’re doing, and that you want the corresponding return values. When I require specific return types, I look to vapply() instead.
vap_chr(list(1, 2, 3:4), force)
#> [1] "1" "2" "3:4"
try(purrr::map_chr(list(1, 2, 3:4), force))
#> Error in purrr::map_chr(list(1, 2, 3:4), force) : ℹ In index: 1.
#> Caused by error:
#> ! Can't coerce from a number to a string.
vap_int(list(1, "1", 1.2), force)
#> [1] 1 1 1
try(purrr::map_int(list(1, "1", 1.2), force))
#> Error in purrr::map_int(list(1, "1", 1.2), force) : ℹ In index: 2.
#> Caused by error:
#> ! Can't coerce from a string to an integer.A feature I’ve grown to love from {purrr} is the easy ability to track progress for each iterations. Where in {purrr} you can quickly add a progress bar with purrr::map(..., .progress = TRUE), {fuj} takes the approach of allowing users to set an option (options("fuj.vap.progress") which triggers tracking indexes and progress. There’s a with_vap_progress() function temporarily sets options to allow for progress tracking:
vap(1:10 / 20, Sys.sleep) |>
with_vap_progress() |>
invisible()
#>
|| 0%
|=======| 10%
|==============| 20%
|=====================| 30%
|============================| 40%
|===================================| 50%
|==========================================| 60%
|=================================================| 70%
|========================================================| 80%
|===============================================================| 90%
|======================================================================| 100%There’s a smaller handler with_vap_indexed_errors() for reporting on indexes of errors or warnings, which may be useful for debugging. These are turned off by default, since tracking the index (in the current form) is not particularly efficient, and you may only need to know these things when debugging or watching something run.
This is a little extra sugar but something I’m looking forward to using. By implementing new methods for / and +2, I can now write file paths like this:
2 Because of how R evaluates operations (see Operator Syntax and Precedence), mixing operations for file path generation doesn’t work too well. All / are evaluated prior to +, which means the LHS of + is coerced to a file_path object, allowing + to sit at the end of the operation, functioning as a file extension helper.
This is similar to what you can do with {fs}:
I would recommend looking into {cnd} for advanced conditions within packages (something that is coming soon to {mark}), but {fuj} has had a new_condition() function for a while. There are some enhancements included which will help keep {fuj} aligned with {cnd}, the latter of which will have more features and improvements included in the future.
new_condition(pkg) -> new_condition(package)new_condition(msg) -> new_condition(message)new_condition() is used within other function which write specific conditions.
example_message <- function(...) {
new_condition(
message = c(...),
type = "message",
class = "example_message",
package = "YourPackageName"
)
}
message(example_message("hello"))
#> <YourPackageName::example_message> hello
message(example_message("there"))
#> <YourPackageName::example_message> there
example_message("handle") |>
message() |>
withCallingHandlers(
example_message = \(c) cat("got it!\n"),
`YourPackageName::example_message` = \(c) cat("got it again!\n")
)
#> got it!
#> got it again!
#> <YourPackageName::example_message> handle{fuj} now comes some defaults such as value_error(), input_error(), class_error(), type_error() and similar warnings.3 These defaults don’t require a package argument, and have been used to replace many of the conditions signaled in {fuj} from previous versions. This should simply errors and warnings.
3 Many of these functions will also be available in a future update to {cnd}. The function names will be identical, so caution will be advices if both {fuj} and {cnd} are both attached to the search path as conflicts will arise. {fuj} implements this with limited functionality, whereas {cnd} provides more advanced message controls.
base::Filter() is fine, but we could do better. The new hold() and toss() functions provide another way to retain or remove elements from a vector, based on a predicate function. These also have a specific NA option, to determine how NA values, returned by the predicate function, should be handled.
x <- c(1, NA, 3, 4, Inf, 6)
twos <- function(x) x %% 2 == 0
hold(x, twos) # 4, 6
#> [1] 4 6
toss(x, twos) # 1, 3, NA, Inf
#> [1] 1 NA 3 Inf
hold(x, twos, na = "keep") # NA, 4, Inf, 6
#> [1] NA 4 Inf 6
toss(x, twos, na = "drop") # 1, 3
#> [1] 1 3
i <- c(1:3, NA)
x <- letters[1:5]
hold(x, i)
#> [1] "a" "b" "c"
toss(x, i)
#> [1] "d" "e"Here’s an example of tracking good and bad credit card numbers using the Luhn algorithm.
luhn <- function(x) {
x <- rev(as.integer(strsplit(as.character(x), "")[[1L]]))
checksum <- 0L
for (i in seq_along(x)[-1L]) {
if (i %% 2L == 0) {
v <- x[i] * 2L
} else {
v <- x[i]
}
if (v > 9L) {
v <- v - 9L
}
checksum <- checksum + v
}
(10 - checksum %% 10) %% 10 == x[1L]
}
numbers <- c(
17893729974,
63663221787,
54698928499,
NA,
24273627513,
79263054159,
43092394809,
NA,
56731000415,
22605612268,
25519297714,
NA
)
hold(numbers, luhn)
#> [1] 17893729974 24273627513 25519297714
toss(numbers, luhn)
#> [1] 63663221787 54698928499 NA 79263054159 43092394809 NA
#> [7] 56731000415 22605612268 NA---
title: "`{fuj} 0.3.0` release"
subtitle: "Enhancements, mostly for me"
date: "2026-10-03"
categories: ["R", "{fuj}"]
draft: true
---
```{r}
library(fuj)
```
This minor release of `{fuj}` presents some new features I'm very excited about and have fun working on.
`{fuj}` is a sort of _basic_ dependency package that I use within some of my own personal projects and within my professional work.
These are mostly simple functions that allow me to reduce dependencies for long-term use.
In some cases, `{fuj}` contains functions that are inspired by other packages (as is the case with the first section on here) but may be implemented differently, with some changes in behaviors, limited features, or scope reduced to fit within the available functionality of `{base}` **R**.[^poorman]
This isn't because I think think I can do it _better_, or _faster_ -- and often times, these are missing features and might be less efficient -- but so I can be lazier with maintenance.
## `vap`
We all know (and love) [`{purrr}`](https://purrr.tidyverse.org/)'s [`map()`](https://purrr.tidyverse.org/reference/map.html) functions.
This is a case of _reinvention_ and complexity reduction so I can use some of the same features without the full dependency on `{purrr}`.
Thus, `vap` functions.
These are _vector apply_ functions, with a few small differences.
For the most part:
| `{fuj}` | `{purrr}` |
|:----------------------------------------------------------------|:----------------------------------------------------------------|
| `vap()`, `vap2()`, `vapi()`, `vapp()` | `map()`, `map2()`, `imap()`, `pmap()` |
| `vap3()` | _(none)_ |
| `vap_lgl()`, `vap_int()`, `vap_dbl()`, `vap_chr()`, `vap_raw()` | `map_lgl()`, `map_int()`, `map_dbl()`, `map_chr()`, `map_raw()` |
| `vap_cpl()`, `vap_date()`, `vap_dttm()` | _(none)_ |
And these extend to `vap2_*()`, `vap3_*()`, `vapi_*()`, `vapp_*()` functions.
All `{fuj}` functions rely on _coercing_ rather than _validating_, like the `{purrr}` functions do.
This makes the functions a little more flexible, but at the cost of potentially causing downstream errors.
`{fuj}` presumes that you know what you're doing, and that you _want_ the corresponding return values.
When I require specific return types, I look to `vapply()` instead.
```{r}
vap_chr(list(1, 2, 3:4), force)
try(purrr::map_chr(list(1, 2, 3:4), force))
vap_int(list(1, "1", 1.2), force)
try(purrr::map_int(list(1, "1", 1.2), force))
```
A feature I've grown to love from `{purrr}` is the easy ability to track progress for each iterations.
Where in `{purrr}` you can quickly add a progress bar with `purrr::map(..., .progress = TRUE)`, `{fuj}` takes the approach of allowing users to set an option (`options("fuj.vap.progress"`) which triggers tracking indexes and progress.
There's a `with_vap_progress()` function temporarily sets options to allow for progress tracking:
```{r}
vap(1:10 / 20, Sys.sleep) |>
with_vap_progress() |>
invisible()
```
There's a smaller handler `with_vap_indexed_errors()` for reporting on indexes of errors or warnings, which may be useful for debugging.
These are turned off by default, since tracking the index (in the current form) is not particularly efficient, and you may only need to know these things when debugging or _watching_ something run.
## File paths
This is a little extra _sugar_ but something I'm looking forward to using.
By implementing new methods for `/` and `+`[^operators], I can now write file paths like this:
```{r}
fp("~") / "Documents" / "foo" + "txt"
```
This is similar to what you can do with `{fs}`:
```{r}
fs::path("~") / "Documents" / "foo" + ".txt"
```
## Conditions
I would recommend looking into [`{cnd}`](https://jmbarbone.github.io/cnd) for advanced conditions within packages (something that is coming _soon_ to `{mark}`), but `{fuj}` has had a `new_condition()` function for a while.
There are some enhancements included which will help keep `{fuj}` aligned with `{cnd}`, the latter of which will have more features and improvements included in the future.
* `new_condition(pkg)` -> `new_condition(package)`
* `new_condition(msg)` -> `new_condition(message)`
`new_condition()` is used within other `function` which write specific conditions.
```{r}
example_message <- function(...) {
new_condition(
message = c(...),
type = "message",
class = "example_message",
package = "YourPackageName"
)
}
message(example_message("hello"))
message(example_message("there"))
example_message("handle") |>
message() |>
withCallingHandlers(
example_message = \(c) cat("got it!\n"),
`YourPackageName::example_message` = \(c) cat("got it again!\n")
)
```
`{fuj}` now comes some _defaults_ such as `value_error()`, `input_error()`, `class_error()`, `type_error()` and similar warnings.[^cnd]
These _defaults_ don't require a `package` argument, and have been used to replace many of the conditions signaled in `{fuj}` from previous versions.
This should simply errors and warnings.
```{r}
some_value <- function(x) {
if (!is.numeric(x)) {
stop(type_error("x must be numeric"))
}
x
}
some_value(1)
try(some_value("a"))
```
## Hold and toss
`base::Filter()` is _fine_, but we could do better.
The new `hold()` and `toss()` functions provide another way to retain or remove elements from a vector, based on a predicate function.
These also have a specific `NA` option, to determine how `NA` values, returned by the predicate function, should be handled.
```{r}
x <- c(1, NA, 3, 4, Inf, 6)
twos <- function(x) x %% 2 == 0
hold(x, twos) # 4, 6
toss(x, twos) # 1, 3, NA, Inf
hold(x, twos, na = "keep") # NA, 4, Inf, 6
toss(x, twos, na = "drop") # 1, 3
i <- c(1:3, NA)
x <- letters[1:5]
hold(x, i)
toss(x, i)
```
Here's an example of tracking good and bad credit card numbers using the [Luhn algorithm](https://en.wikipedia.org/wiki/Luhn_algorithm).
```{r}
luhn <- function(x) {
x <- rev(as.integer(strsplit(as.character(x), "")[[1L]]))
checksum <- 0L
for (i in seq_along(x)[-1L]) {
if (i %% 2L == 0) {
v <- x[i] * 2L
} else {
v <- x[i]
}
if (v > 9L) {
v <- v - 9L
}
checksum <- checksum + v
}
(10 - checksum %% 10) %% 10 == x[1L]
}
numbers <- c(
17893729974,
63663221787,
54698928499,
NA,
24273627513,
79263054159,
43092394809,
NA,
56731000415,
22605612268,
25519297714,
NA
)
hold(numbers, luhn)
toss(numbers, luhn)
```
[^poorman]: There's a wonderful project [`{poorman}`](https://nathaneastwood.github.io/poorman/) which replicates many [`{dplyr}`](https://dplyr.tidyverse.org/) functions, using just `{base}` **R**.
[^cnd]: Many of these functions will also be available in a future update to [`{cnd}`](https://jmbarbone.github.io/cnd).
The function names will be identical, so caution will be advices if both `{fuj}` and `{cnd}` are both attached to the search path as conflicts will arise.
`{fuj}` implements this with limited functionality, whereas `{cnd}` provides more advanced message controls.
[^operators]: Because of how R evaluates operations (see [Operator Syntax and Precedence](https://stat.ethz.ch/R-manual/R-devel/library/base/html/Syntax.html)), mixing operations for file path generation doesn't work too well.
All `/` are evaluated prior to `+`, which means the LHS of `+` is coerced to a `file_path` object, allowing `+` to sit at the end of the operation, functioning as a file extension helper.