Parametrized active bindings
Clone or download
krlmlr rename fun to bindr_fun
- The call stack for a failure in an active binding now contains a reference to `bindr_fun()` instead of `fun()`, to hint at what might have went wrong.
Latest commit b6e6fd6 Mar 19, 2018
Permalink
Failed to load latest commit information.
R rename fun to bindr_fun Mar 19, 2018
man NEWS Feb 5, 2018
tests test payload Nov 11, 2016
.Rbuildignore ignore, finally Nov 12, 2016
.gitignore ignore, finally Nov 12, 2016
.travis.yml up pkgdown Feb 5, 2018
DESCRIPTION Deploy from Travis build 68 [ci skip] Mar 13, 2018
LICENSE copyright Nov 8, 2016
NAMESPACE document Oct 15, 2016
NEWS.md Deploy from Travis build 68 [ci skip] Mar 13, 2018
README.Rmd up README Mar 13, 2018
README.md up README Mar 13, 2018
_pkgdown.yml up pkgdown Feb 5, 2018
appveyor.yml CI Oct 15, 2016
bindr.Rproj infra Oct 15, 2016
codecov.yml add badge Oct 15, 2016
cran-comments.md CRAN comments Mar 13, 2018
tic.R up pkgdown Feb 5, 2018

README.md

bindr Travis-CI Build Status AppVeyor Build Status Coverage Status CRAN_Status_Badge

Active bindings in R are much like properties in other languages: They look like a variable, but querying or setting the value triggers a function call. They can be created in R via makeActiveBinding(), but with this API the function used to compute or change the value of a binding cannot take additional arguments. The bindr package faciliates the creation of active bindings that are linked to a function that receives the binding name, and an arbitrary number of additional arguments.

Installation

You can install bindr from GitHub with:

# install.packages("devtools")
devtools::install_github("krlmlr/bindr")

Getting started

For illustration, the append_random() function is used. This function appends a separator (a dash by default) and a random letter to its input, and talks about it, too.

set.seed(20161510)
append_random <- function(x, sep = "-") {
  message("Evaluating append_random(sep = ", deparse(sep), ")")
  paste(x, sample(letters, 1), sep = sep)
}

append_random("a")
#> Evaluating append_random(sep = "-")
#> [1] "a-k"
append_random("X", sep = "+")
#> Evaluating append_random(sep = "+")
#> [1] "X+u"

In this example, we create an environment that contains bindings for all lowercase letters, which are evaluated with append_random(). As a result, a dash and a random letter are appended to the name of the binding:

library(bindr)
env <- create_env(letters, append_random)
ls(env)
#>  [1] "a" "b" "c" "d" "e" "f" "g" "h" "i" "j" "k" "l" "m" "n" "o" "p" "q"
#> [18] "r" "s" "t" "u" "v" "w" "x" "y" "z"
env$a
#> Evaluating append_random(sep = "-")
#> [1] "a-p"
env$a
#> Evaluating append_random(sep = "-")
#> [1] "a-j"
env$a
#> Evaluating append_random(sep = "-")
#> [1] "a-b"
env$c
#> Evaluating append_random(sep = "-")
#> [1] "c-b"
env$Z
#> NULL

Bindings can also be added to existing environments:

populate_env(env, LETTERS, append_random, "+")
env$a
#> Evaluating append_random(sep = "-")
#> [1] "a-z"
env$Z
#> Evaluating append_random(sep = "+")
#> [1] "Z+j"

Further properties

Both named and unnamed arguments are supported:

create_env("binding", paste, "value", sep = "-")$binding
#> [1] "binding-value"

A parent environment can be specified for creation:

env2 <- create_env("a", identity, .enclos = env)
env2$a
#> a
env2$b
#> NULL
get("b", env2)
#> Evaluating append_random(sep = "-")
#> [1] "b-m"

The bindings by default have access to the calling environment:

create_local_env <- function(names) {
  paste_with_dash <- function(...) paste(..., sep = "-")
  binder <- function(name, append) paste_with_dash(name, append)
  create_env(names, binder, append = "appending")
}

env3 <- create_local_env("a")
env3$a
#> [1] "a-appending"

All bindings are read-only:

env3$a <- NA
#> Error: Binding is read-only.
env3$a <- NULL
#> Error: Binding is read-only.

Existing variables or bindings are not overwritten:

env4 <- as.environment(list(a = 5))
populate_env(env4, list(quote(b)), identity)
ls(env4)
#> [1] "a" "b"
populate_env(env4, letters, identity)
#> Error in populate_env(env4, letters, identity): Not creating bindings for existing variables: b, a

Active bindings and C++

Active bindings must be R functions. To interface with C++ code, one must bind against an exported Rcpp function, possibly with rng = false if performance matters. The bindrcpp package uses bindr to provide an easy-to-use C++ interface for parametrized active bindings, and is the recommended way to interface with C++ code. In the remainder of this section, an alternative using an exported C++ function is shown.

The following C++ module exports a function change_case(to_upper = FALSE), which is bound against in R code later.

#include <Rcpp.h>

#include <algorithm>
#include <string>

using namespace Rcpp;

// [[Rcpp::export(rng = FALSE)]]
SEXP change_case(Symbol name, bool to_upper = false) {
  std::string name_string = name.c_str();
  std::transform(name_string.begin(), name_string.end(),
                 name_string.begin(), to_upper ? ::toupper : ::tolower);
  return CharacterVector(name_string);
}

Binding from R:

env <- create_env(list(as.name("__ToLower__")), change_case)
populate_env(env, list(as.name("__tOuPPER__")), change_case, TRUE)
ls(env)
#> [1] "__ToLower__" "__tOuPPER__"
env$`__ToLower__`
#> [1] "__tolower__"
get("__tOuPPER__", env)
#> [1] "__TOUPPER__"