Anonymous Functions in R - Part 2

An Anonymous Function (also known as a lambda experssion) is a function definition that is not bound to an identifier. That is, it is a function that is created and used, but never assigned to a variable.

In a prior post I discussed how I am not completely satisfied with purrr/rlang’s anonymous function syntax as it has no capability for naming the function arguments anything other than .x and .y.

In this post, I’ll look at alternate syntaxes for creating anonymous functions.

Implementations of anonymous functions in R

There are many implementations of helpers to create anonymous functions.

Base R has its own mechanism, and all the other implementations try and keep the syntax shorter by dropping the requirement to actually type function() when creating the anonymous function.

The following table lists the implementations of anonymous functions I could find in R, and their syntax for creating an anonymous function with 2 arguments.

The Formal Arguments column indicates how the arguments for the anonymous function are derived. Some packages support both implicit and explicit argument naming (e.g. gsubfn and pryr). If the formal arguments are not explicitly stated, then the argument list may be inferred by:

  • convention e.g. rlang::as_function() assumes arguments are .x and .y
  • inference e.g. pryr::f() uses all variables found in the expression as formal arguments.
Package Function Name 1-arg 2-args Explicit Arg Names Formal Arguments Extra chars for implicit args Core Syntax Core Syntax Nchars formals/body separator comments author URL
base r function function(x) x + 1 function(x, y) x + y Yes Explicit NA function() 10 NA NA R Core NA
rlang as_function as_function(~.x + 1)) as_function(~.x+.y) No Implicit. Only .x or .y .x f(~) 4 NA used within purrr Lionel Henry et al
pryr f (implicit args) f(x + 1) f(x + y) No Implicit. Inferred from expression NA f() 3 NA NA Hadley Wickham
pryr f (explicit args) f(x, x + 1) f(x, y, x + y) Yes Explicit NA f() 3 , NA NA NA
nofrills fn fn(x ~ x + 1) fn(x, y ~ x + y) Yes Explicit NA f(~) 4 ~ Fully NSE aware (!!) Eugene Ha
gsubfn as.function.formula (implicit args) as.function.formula(~ x + 1) as.function.formula(~ x + y) No Implicit. Inferred from RHS of formula NA f(~) 4 NA NA G. Grothendieck
gsubfn as.function.formula (explicit args) as.function.formula(x ~ x + 1) as.function.formula(x + y ~ x + y + z) Yes Explicit. Inferred from LHS of formula NA f(~) 4 ~ NA NA NA
wrapr lambda lambda(x, x + 1) lambda(x, y, x + y) Yes Explicit NA f() 3 , NA John Mount et al
lambda f f(.(x) + 1) f(.(x) + .(y)) No Implicit. Inferred from expression .(x) f() 3 NA NA Jim Hester
lambdass ~~ ~~ ..1 + 1 ~~ ..1 + ..2 No Implicit. Only ..N arg syntax allowed ..1 ~~ 2 NA NA TobCap
lambdass f.() f.(x, x + 1) f.(x, y, x + y) Yes Explicit NA f() 3 , NA NA NA
lambdass %->% f(x) %->% {x + 1} f(x, y) %->% {x + y} Yes Explicit NA f()%->%{} 9 `%->% NA NA NA
functional “->” NA x ~ y -> x + y Yes Explicit NA NA NA NA messes with <- operator! Konrad Rudolph
Not packaged lambda lambda(x ~ x + 1L) lambda(x + y ~ x + y) Yes Explicit. Inferred from LHS of formula NA f(~) 4 ~ NA Edward Visel
Not packaged lambda lambda(x:x + 1) lambda(x, y:x + y) Yes Explicit NA f(:) 4 , Code no longer available Koji MAKIYAMA
Not packaged [] -> [x] -> x + 1 [x, y] -> x + y Yes Explicit NA []-> 4 `-> Speculative Future R syntax Lionel Henry

Features of an anonymous function syntax

I’ve tried to come up with a taxonomy to help classify the various syntaxes. I can’t say I was hugely successful, but it helped me organise my thoughts a little.

  • Explicit Arguments: The syntax has a list of formal arguments provided separate to the body of the function
    1. Arguments provided as a separate list
      • e.g. Base R
    2. Arguments are parsed out of a separate expression (which is not the expression which becomes the body of the anonymous function)
      • e.g. gsubfn where names are parsed out of the expression on the LHS of the formula
  • Implicit Arguments: The syntax only provides the body of the anonymous function, and the formal arguments are never separately listed
    1. Arguments are pre-defined
      • e.g. rlang::as_function pre-defines .x and .y as the formal args.
      • The lambdass package uses the the ..1, ..2 argument form in one of its implementations.
    2. Arguments are parsed from the expression which becomes the function body
      • .e.g the one-sided formula from gsubfn
  • Implicit Arguments with discrimination: arguments are parsed from the given expression, and there is some way to indicate whether this is a variable from the enclosing environment or is a formal argument.
    • Only Jim Hester’s lambda does this. Variables specified using the bquote() syntax of .(x) are considered formal arguments, everything else is a variable from the enclosing environment.
  • Function Creation
    1. Function Call: Use a short function call to hide the explicit call to function().
      • This is the most common route of creation e.g. rlang::as_function(), pryr::f()
    2. Exsiting Operator: Existing operator is repurposed to create function
      • e.g. lambdass hijacks the operation of ~
      • e.g. Lionel Henry’s syntax idea hijacks the -> operator
      • e.g. Konrad Rudolph’s functional package/module also hijacks the -> operator and thus is only useable in code which uses = for assignment.
    3. New operator: New operator used to create function
      • Only lambdass does this with the %->% infix operator

Notes on current implementations

Some of my thoughts/notes on the current implementations

  • Base R
    • Some would consider having to write out function() {} makes this a verbose syntax. Others would say I’m wrong.
    • The function() text and the separate explicit list of formal arguments seems mostly redundant for a large percentage of cases where anonymous functions must be used i.e. single formal argument and small function body.
  • rlang::as_function()
    • Very handy within the purrr family of functions.
    • BUT: No ability to customise argument names!
  • pryr::f()
    • Supports both implicit and explicit formal arguments
    • Doesn’t use the leading ~ like purrr/rlang.
  • nofrills::fn()
    • Only explicit formal arguments
    • Uses ~
    • Full support for tidyverse style Non-standard evaluation! This seems like overkill for my use case.
  • gsubfn::as.function.formula()
    • Supports both implicit and explicit formal arguments
    • Uses a leading ~ like purrr/rlang.
    • When using explicit formal args, the LHS (confusingly) looks like an expression itself!
  • wrapr::lambda()
    • Explicit formal arguments only
    • Doesn’t use leading ~
  • Jim Hester’s lambda
    • I like that it allows you to explicitly indicate variables in the expression body that should be formal arguments using bquote() substitution syntax. I just find the bquote() substitution syntax too fiddly/verbose!
  • lambdass::f.()
    • Explicit formal arguments only
    • The other two forms for creating anonymous functions are a bit too funky for my liking!
      • Overriding the operation of ~ seems fraught with issues
      • the %->% operator seems too fiddly/verbose
  • functional module
    • The use of the forward assignment operator looks nice and mimics how other languages might do an anonymous function.
    • However, because it uses the ->, it actually messes with <- as well, so you have to use = to do actual assignment in your code if you use this.

Thoughts on unifying implementation

After having looked at the existing implementations, here is my short list of what I think is needed for a good anonymous function implementation in R

  • Use a function call to create the anonymous function - don’t use operator overloading or create a new operator.
  • Allow both explicit and implicit formal arguments to give brevity when formal arguments are obvious, but flexibilty to explicitly define things
  • Compatibility with rlang syntax. Ultimately, purrr::map is where I want to use anonymous functions the most, so it’d be nice if this could be a drop-in replacement.

Next steps

Future post: What might a unifying implementation look like?