Draft

Enhancements, mostly for me

R
{fuj}
Published

October 3, 2026

Code
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.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.

1 There’s a wonderful project {poorman} which replicates many {dplyr} functions, using just {base} R.

vap

We 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.

Code
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:

Code
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.

File paths

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.

Code
fp("~") / "Documents" / "foo" + "txt"
#> ~/Documents/foo.txt

This is similar to what you can do with {fs}:

Code
fs::path("~") / "Documents" / "foo" + ".txt"
#> ~/Documents/foo.txt

Conditions

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.

Code
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.

Code
some_value <- function(x) {
  if (!is.numeric(x)) {
    stop(type_error("x must be numeric"))
  }
  x
}
some_value(1)
#> [1] 1
try(some_value("a"))
#> Error : <type_error> x must be numeric

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.

Code
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.

Code
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