compose: Compose Functions

View source: R/compose.R

composeR Documentation

Compose Functions

Description

To compose functions,

  • Use compose():

      compose(f, g, h, ...)
    

    This makes the function that applies f, then g, then h, etc. It has the formals of the first function applied (namely f). For example, if

      fun <- compose(paste, toupper)
    

    then the function fun() has the same signature as paste(), and the call

      fun(letters, collapse = ",")
    

    is equivalent to the composite call

      toupper(paste(letters, collapse = ","))
    
  • Use `%>>>%`:

      f %>>>% g %>>>% h %>>>% ...
    

    It comprehends both the semantics of the magrittr `%>%` operator and quasiquotation. For example, if

      sep <- ""
      fun <- sample %>>>% paste(collapse = !!sep)
    

    then the function fun() has the same signature as sample(), and the call

      fun(x, size, replace, prob)
    

    is equivalent to the composite call

      paste(sample(x, size, replace, prob), collapse = "")
    

Use as.list() to recover the list of composite functions. For example, both

  as.list(compose(paste, capitalize = toupper))

  as.list(paste %>>>% capitalize: toupper)

return the (named) list of functions list(paste, capitalize = toupper).

Usage

compose(...)

fst %>>>% snd

Arguments

...

Functions or lists thereof to compose, in order of application. Lists of functions are automatically spliced in. Unquoting of names, via !! on the left-hand side of :=, and splicing, via !!!, are supported.

fst, snd

Functions. These may be optionally named using a colon (:), e.g., f %>>>% nm: g names the g-component "nm" (see ‘Exceptions to the Interpretation of Calls as Functions’). Quasiquotation and the magrittr `%>%` semantics are supported (see ‘Semantics of the Composition Operator’, ‘Quasiquotation’ and ‘Examples’).

Value

Function of class CompositeFunction, whose formals are those of the first function applied (as a closure).

Semantics of the Composition Operator

The `%>>>%` operator adopts the semantics of the magrittr `%>%` operator:

  1. Bare names are matched to functions: For example, in a composition like

      ... %>>>% foo %>>>% ...
    

    the ‘foo’ is matched to the function of that name.

  2. Function calls are interpreted as a unary function of a point (.): A call is interpreted as a function (of a point) in one of two ways:

    • If the point matches an argument value, the call is literally interpreted as the body of the function. For example, in the compositions

        ... %>>>% foo(x, .) %>>>% ...
      
        ... %>>>% foo(x, y = .) %>>>% ...
      

      the ‘foo(x, .)’, resp. ‘foo(x, y = .)’, is interpreted as the function function(..., . = ..1) foo(x, .), resp. function(..., . = ..1) foo(x, y = .).

    • Otherwise, the call is regarded as implicitly having the point as its first argument before being interpreted as the body of the function. For example, in the compositions

        ... %>>>% foo(x) %>>>% ...
      
        ... %>>>% foo(x, y(.)) %>>>% ...
      

      the ‘foo(x)’, resp. ‘foo(x, y(.))’, is interpreted as the function function(..., . = ..1) foo(., x), resp. function(..., . = ..1) foo(., x, y(.)).

  3. Expressions {...} are interpreted as a function of a point (.): For example, in a composition

      ... %>>>% {
        foo(.)
        bar(.)
      } %>>>% ...
    

    the ‘{foo(.); bar(.)}’ is interpreted as the function function(..., . = ..1) {foo(.); bar(.)}.

    Curly braces are useful when you need to circumvent `%>>>%`'s usual interpretation of function calls. For example, in a composition

      ... %>>>% {foo(x, y(.))} %>>>% ...
    

    the ‘{foo(x, y(.))}’ is interpreted as the function function(..., . = ..1) foo(x, y(.)). There is no point as first argument to foo.

Exceptions to the Interpretation of Calls as Functions

As a matter of convenience, some exceptions are made to the above interpretation of calls as functions:

  • Parenthesis (() applies grouping. (In R, `(` is indeed a function.) In particular, expressions within parentheses are literally interpreted.

  • Colon (:) applies naming, according to the syntax ‘<name>: <function>’, where ‘<function>’ is interpreted according to the semantics of `%>>>%`. For example, in

      ... %>>>% aName: foo %>>>% ...
    

    the function foo is named "aName".

  • fn(), namespace operators (`::` , `:::`) and extractors (`$`, `[[`, `[`) are literally interpreted. This allows for list extractors to be applied to composite functions appearing in a `%>>>%` call (see 'Operate on Composite Functions as List-Like Objects'). For example, the compositions

      paste %>>>% tolower
    
      paste %>>>% base::tolower
    
      (paste %>>>% toupper)[[1]] %>>>% tolower
    

    are equivalent functions.

Quasiquotation

The `%>>>%` operator supports Tidyverse unquoting (via !!). Use it to:

  • Enforce immutability: For example, by unquoting res in

      res <- "result"
      get_result <- identity %>>>% lapply(`[[`, !!res)
    

    you ensure that the function get_result() always extracts the component named "result", even if the binding res changes its value or is removed altogether.

  • Interpret the point (.) in the lexical scope: Even though `%>>>%` interprets ‘.’ as a function argument, you can still reference an object of that name via unquoting. For example,

      . <- "point"
      is_point <- identity %>>>% {. == !!.}
    

    determines a function that checks for equality with the string "point".

  • Name composite functions, programmatically: For example, unquoting nm in

      nm <- "aName"
      ... %>>>% !!nm: foo %>>>% ...
    

    names the ‘foo’-component of the resulting composite function "aName".

  • Accelerate functions by fixing constant dependencies: For example, presuming the value of the call f() is constant and that g is a pure function (meaning that its return value depends only on its input), both

      ... %>>>% g(f()) %>>>% ...
    
      ... %>>>% g(!!f()) %>>>% ...
    

    would be functions yielding the same values. But the first would compute f() anew with each call, whereas the second would simply depend on a fixed, pre-computed value of f().

Operate on Composite Functions as List-Like Objects

You can think of a composite function as embodying the (possibly nested) structure of its list of constituent functions. In fact, you can apply familiar index and assignment operations to a composite function, as if it were this list, getting a function in return. This enables you to leverage composite functions as structured computations.

Indexing

For instance, the ‘sum’ in the following composite function

  f <- abs %>>>% out: (log %>>>% agg: sum)

can be extracted in the usual ways:

  f[[2]][[2]]
  f[[c(2, 2)]]

  f$out$agg
  f[["out"]][["agg"]]
  f[["out"]]$agg

  f$out[[2]]
  f[[list("out", 2)]]

The last form of indexing with a mixed list is handy when you need to create an index programmatically.

Additionally, you can excise sub-composite functions with [, head(), tail(). For example:

  • Both f[1] and head(f, 1) get the ‘abs’ as a composite function, namely compose(abs)

  • f[2:1] reverses the order of the top-level functions to yield

      out: (log %>>>% agg: sum) %>>>% abs
    
  • f$out[c(FALSE, TRUE)] gets the ‘sum’ as a (named) composite function

Subset Assignment

Similarily, subset assignment works as it does for lists. For instance, you can replace the ‘sum’ with the identity function:

  f[[2]][[2]] <- identity

  f$out$agg <- identity
  f[["out"]][["agg"]] <- identity

  f$out[[2]] <- identity
  f[[list("out", 2)]] <- identity

Multiple constituent functions can be reassigned using [<-. For example

  f[2] <- list(log)

  f["out"] <- list(log)

  f[c(FALSE, TRUE)] <- list(log)

all replace the second constituent function with log, so that f becomes abs %>>>% log.

Other List Methods

The generic methods unlist(), length(), names() also apply to composite functions. In conjunction with compose(), you can use unlist() to “flatten” compositions. For example

  compose(unlist(f, use.names = FALSE))

gives a function that is identical to

  abs %>>>% log %>>>% sum

Composite Functions Balance Speed and Complexity

The speed of a composite function made by compose() or `%>>>%` (regardless of its nested depth) is on par with a manually constructed serial composition. This is because compose() and `%>>>%` are associative, semantically and operationally. For instance, triple compositions,

  compose(f, g, h)
  f %>>>% g %>>>% h

  compose(f, compose(g, h))
  f %>>>% (g %>>>% h)

  compose(compose(f, g), h)
  (f %>>>% g) %>>>% h

are all implemented as the same function. Lists of functions are automatically “flattened” when composed.

Nevertheless, the original nested structure of constituent functions is faithfully recovered by as.list(). In particular, as.list() and compose() are mutually invertible: as.list(compose(fs)) is the same as fs, when fs is a (nested) list of functions. (But note that the names of the list of composite functions is always a character vector; it is never NULL.)

See Also

constant(); combined with `%>>>%`, this provides a lazy, structured alternative to the magrittr `%>%` operator.

Examples

# Functions are applied in the order in which they are listed
inv <- partial(`/`, 1)  # reciprocal
f0 <- compose(abs, log, inv)
stopifnot(all.equal(f0(-2), 1 / log(abs(-2))))

# Alternatively, compose using the `%>>>%` operator
f1 <- abs %>>>% log %>>>% {1 / .}
stopifnot(all.equal(f1(-2), f0(-2)))

## Not run: 
# Transform a function to a JSON function
library(jsonlite)

# By composing higher-order functions:
jsonify <- {fromJSON %>>>% .} %>>>% {. %>>>% toJSON}

# By directly composing with input/output transformers:
jsonify <- fn(f ~ fromJSON %>>>% f %>>>% toJSON)
## End(Not run)

# Formals of initial function are preserved
add <- function(a, b = 0) a + b
stopifnot(identical(formals(compose(add, inv)), formals(add)))

# Compositions can be provided by lists, in several equivalent ways
f2 <- compose(list(abs, log, inv))
f3 <- compose(!!! list(abs, log, inv))
f4 <- compose(abs, list(log, inv))
f5 <- compose(abs, !!! list(log, inv))
stopifnot(
  all.equal(f2, f0), all.equal(f2(-2), f0(-2)),
  all.equal(f3, f0), all.equal(f3(-2), f0(-2)),
  all.equal(f4, f0), all.equal(f4(-2), f0(-2)),
  all.equal(f5, f0), all.equal(f5(-2), f0(-2))
)

# compose() and as.list() are mutally invertible
f6 <- compose(abs, as.list(compose(log, inv)))
stopifnot(
  all.equal(f6, f0), all.equal(f6(-2), f0(-2))
)
fs <- list(abs, log, inv)
stopifnot(all.equal(check.attributes = FALSE,
  as.list(compose(fs)), fs,
))

# `%>>>%` supports names, magrittr `%>%` semantics, and quasiquotation
sep <- ""
scramble <- shuffle: sample %>>>% paste(collapse = !!sep)
nonsense <- scramble(letters)
stopifnot(
  nchar(nonsense) == 26L,
  identical(letters, sort(strsplit(nonsense, sep)[[1]])),
  identical(scramble$shuffle, sample)
)


gestalt documentation built on Aug. 22, 2022, 5:08 p.m.