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.
You can install bindr
from GitHub with:
# install.packages("devtools")
::install_github("krlmlr/bindr") devtools
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)
<- function(x, sep = "-") {
append_random 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)
<- create_env(letters, append_random)
env 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"
$a
env#> Evaluating append_random(sep = "-")
#> [1] "a-p"
$a
env#> Evaluating append_random(sep = "-")
#> [1] "a-j"
$a
env#> Evaluating append_random(sep = "-")
#> [1] "a-b"
$c
env#> Evaluating append_random(sep = "-")
#> [1] "c-b"
$Z
env#> NULL
Bindings can also be added to existing environments:
populate_env(env, LETTERS, append_random, "+")
$a
env#> Evaluating append_random(sep = "-")
#> [1] "a-z"
$Z
env#> Evaluating append_random(sep = "+")
#> [1] "Z+j"
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:
<- create_env("a", identity, .enclos = env)
env2 $a
env2#> a
$b
env2#> NULL
get("b", env2)
#> Evaluating append_random(sep = "-")
#> [1] "b-m"
The bindings by default have access to the calling environment:
<- function(names) {
create_local_env <- function(...) paste(..., sep = "-")
paste_with_dash <- function(name, append) paste_with_dash(name, append)
binder create_env(names, binder, append = "appending")
}
<- create_local_env("a")
env3 $a
env3#> [1] "a-appending"
All bindings are read-only:
$a <- NA
env3#> Error: Binding is read-only.
$a <- NULL
env3#> Error: Binding is read-only.
Existing variables or bindings are not overwritten:
<- as.environment(list(a = 5))
env4 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 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)]]
(Symbol name, bool to_upper = false) {
SEXP change_casestd::string name_string = name.c_str();
std::transform(name_string.begin(), name_string.end(),
.begin(), to_upper ? ::toupper : ::tolower);
name_stringreturn CharacterVector(name_string);
}
Binding from R:
<- create_env(list(as.name("__ToLower__")), change_case)
env populate_env(env, list(as.name("__tOuPPER__")), change_case, TRUE)
ls(env)
#> [1] "__ToLower__" "__tOuPPER__"
$`__ToLower__`
env#> [1] "__tolower__"
get("__tOuPPER__", env)
#> [1] "__TOUPPER__"