Anonymous Functions, Part II: gsubfn

gsubfn's as.function and fn$* notation

Edward Visel

3 minute read

This is a follow-up to Anonymous Functions, Not Variables. For context, read that first.


After my previous post, Brodie Gaslam pointed me in an interesting direction on Twitter:

He’s right. Gabor Grothendieck’s gsubfn package is centered around its gsubfn function, an extended version of gsub whose replacement parameter can accept functions, e.g. to do arithmetic with numbers in a string.

That anonymous function can be written as a formula, a capability enabled by a as.function.formula function which is also exported. It provides very similar functionality to the lambda function I defined, so the nested map example could be written

library(purrr)

map(c("a", "b", "c"), 
    gsubfn::as.function.formula(x ~ map_chr(1:3, 
                            gsubfn::as.function.formula(y ~ paste0(x, y))))) %>% 
    str()
#> List of 3
#>  $ : chr [1:3] "a1" "a2" "a3"
#>  $ : chr [1:3] "b1" "b2" "b3"
#>  $ : chr [1:3] "c1" "c2" "c3"

As it appears, it is a method for the S3 generic base::as.function, so when gsubfn is loaded, the syntax can be condensed to

library(gsubfn)
#> Loading required package: proto

map(c("a", "b", "c"), 
    as.function(x ~ map_chr(1:3, 
                            as.function(y ~ paste0(x, y))))) %>% 
    str()
#> List of 3
#>  $ : chr [1:3] "a1" "a2" "a3"
#>  $ : chr [1:3] "b1" "b2" "b3"
#>  $ : chr [1:3] "c1" "c2" "c3"

and the generic will dispatch to it when passed a formula.

The best part of gsubfn::as.function.formula, though, is the alternative way it can be called. At the end of the previous post, I concluded

Ideally, it would be nice to drop the lambda call altogether, but as far as I have been able to find, the only way to do so would be to redefine purrr:::as_mapper.default to call lambda instead of rlang::as_closure when passed a formula with anything on the left-hand side.

For now, then, extra keystrokes abide.

But gsubfn has found a clever way to minimize keystrokes. It defines an fn object with its own class. That class has a method for $ that is rather different than its usual subsetting duties. Instead, it takes a call with a formula in it, and returns that call with the formula converted to a function by as.function.formula (with some safeguards to ignore actual formulas). So while it looks a little weird, at first, you can write

fn$sapply(1:3, 
          i ~ paste0(letters[i], i))
#> [1] "a1" "b2" "c3"

fn$map2_chr(letters[1:3], 
            1:3, 
            l + n ~ paste0(l, n))
#> [1] "a1" "b2" "c3"

or for the nested map example,

fn$map(c("a", "b", "c"), 
       x ~ fn$map_chr(1:3, 
                      y ~ paste0(x, y))) %>% 
    str()
#> List of 3
#>  $ : chr [1:3] "a1" "a2" "a3"
#>  $ : chr [1:3] "b1" "b2" "b3"
#>  $ : chr [1:3] "c1" "c2" "c3"

effectively modifying functions on the fly to accept full formula lambda notation in a beautiful bit of code.

comments powered by Disqus