Inheriting in Items

June 23, 2011
By

(This article was first published on Struggling Through Problems, and kindly contributed to R-bloggers)

What if we want to extend the behavior of one item with another? Or, to put it another way, what if we want one item to be able to grab the functionality of another?

It turns out we can (almost) get this behavior without modifying the item constructor at all. Here's how you do the grabbing:

> grab = function(src, obj=parent.frame()) {

> for (name in ls(src)) {

> (function(name) {

> if (!exists(name, obj, inh=F)) {

> delayedAssign(name, src[[name]], assign.env=obj)

> }

> })(name)

> }

> }

Now let's use that:

> A = item(

> x =,

> y =,

>

> sum = x + y

> )

> B = item(

> x =,

> y =,

>

> init = grab(A(x, y))

> )

> b = B(2, 3)

> as.list(b)

$sum
[1] 2

$x
[1] 3

$y
NULL

What is going on here?? This one is really tricky; it took me 1/2 hour or so to figure it out.

So, clearly, the problem starts with the fact that init is evaluated too late. We could easily fix the problem like this:

> b = B(2, 3)

> force(b$init)

NULL

> as.list(b)

$sum
[1] 5

$x
[1] 2

$y
[1] 3

$init
NULL

But what's with the argument values appearing with the wrong names? Here's the C function that implements as.list.environment:

    1 SEXP attribute_hidden do_env2list(SEXP call, SEXP op, SEXP args, SEXP rho)
    2 {
    3     SEXP env, ans, names;
    4     int k, all;
    5 
    6     checkArity(op, args);
    7 
    8     env = CAR(args);
    9     if (ISNULL(env))
   10     error(_("use of NULL environment is defunct"));
   11     if( !isEnvironment(env) ) {
   12         SEXP xdata;
   13     if( IS_S4_OBJECT(env) && TYPEOF(env) == S4SXP &&
   14         (xdata = R_getS4DataSlot(env, ENVSXP)) != R_NilValue)
   15         env = xdata;
   16     else
   17         error(_("argument must be an environment"));
   18     }
   19 
   20     all = asLogical(CADR(args));
   21     if (all == NA_LOGICAL) all = 0;
   22 
   23     if (env == R_BaseEnv || env == R_BaseNamespace)
   24     k = BuiltinSize(all, 0);
   25     else if (HASHTAB(env) != R_NilValue)
   26     k = HashTableSize(HASHTAB(env), all);
   27     else
   28     k = FrameSize(FRAME(env), all);
   29 
   30     PROTECT(names = allocVector(STRSXP, k));
   31     PROTECT(ans = allocVector(VECSXP, k));
   32 
   33     k = 0;
   34     if (env == R_BaseEnv || env == R_BaseNamespace)
   35     BuiltinValues(all, 0, ans, &k);
   36     else if (HASHTAB(env) != R_NilValue)
   37     HashTableValues(HASHTAB(env), all, ans, &k);
   38     else
   39     FrameValues(FRAME(env), all, ans, &k);
   40 
   41     k = 0;
   42     if (env == R_BaseEnv || env == R_BaseNamespace)
   43     BuiltinNames(all, 0, names, &k);
   44     else if (HASHTAB(env) != R_NilValue)
   45     HashTableNames(HASHTAB(env), all, names, &k);
   46     else
   47     FrameNames(FRAME(env), all, names, &k);
   48 
   49     setAttrib(ans, R_NamesSymbol, names);
   50     UNPROTECT(2);
   51     return(ans);
   52 }
   53 

Notice that it grabs the values first, and the names second. So when it goes to grab the values, they are 2, 3, and a promise to evaluate grab(A(x, y)). But wait... while it's getting those values, won't the promise be evaluated, and change the structure R is working on?

Yes... and that could be a problem, but it isn't:

    1 static void FrameValues(SEXP frame, int all, SEXP values, int *indx)
    2 {
    3     while (frame != R_NilValue) {
    4     if ((all || CHAR(PRINTNAME(TAG(frame)))[0] != '.') &&
    5                       CAR(frame) != R_UnboundValue) {
    6         SEXP value = CAR(frame);
    7         if (TYPEOF(value) == PROMSXP) {
    8         PROTECT(value);
    9         value = eval(value, R_GlobalEnv);
   10         UNPROTECT(1);
   11         }
   12         SET_VECTOR_ELT(values, *indx, duplicate(value));
   13         (*indx)++;
   14     }
   15     frame = CDR(frame);
   16     }
   17 }
   18 

Values are stored in a pair list, which is like a linked list in that modifications to the front of the list don't affect someone already iterating further down. So grab() sticks sum at the beginning, where it won't show up in the values. And the result of grab() is the NULL that you see in the 3rd value.

Then it goes to get the names. But it's only allocated space for 3 names, because it already checked the size. So the first 3 names are sum, x, and y. And that's what you see. From looking at how FrameNames works, it looks like this should cause a buffer overrun, but that doesn't show up.

We could have an item automatically check for the existence of init at instantiation time and run it if it exists, but I prefer a more general solution:

> item = function(...) {

> foo = function() {

> E = environment()

>

> for (i in seq_along(args)) {

> name = args.names[[i]]

> if (substr(name, 1, 1) == '.') {

> force(E[[name]])

> }

> }

>

> E

> }

>

> args = process.args(as.list(substitute(list(...)))[-1L])

> args.names = names(args)

> formals(foo) = args

>

> foo

> }

That is, we force all arguments starting with a '.'. Then we can modify process.args to give unnamed arguments a name with a '.':

> process.args = function(args) {

> arg.names = names(args)

> if (is.null(arg.names)) {

> arg.names = replicate(length(args), NULL)

> }

>

> for (i in seq_along(args)) {

> if (is.null(arg.names[[i]]) || arg.names[[i]] == '') {

> arg.names[[i]] = paste('.', as.character(i), sep='')

> }

> }

>

> names(args) = arg.names

>

> args

> }

Now we can write:

> B = item(

> x =,

> y =,

>

> grab(A(x, y))

> )

> b = B(2, 3)

> as.list(b)

$sum
[1] 5

$name
[1] ".3"

$i
[1] 3

$E


$x
[1] 2

$y
[1] 3

Oh yeah we have some extra junk in there. We could hide it with '.' names, or we could hide everything by nesting:

> item = function(...) {

> foo = function() {

> (function() {

> E = parent.frame()

>

> for (i in seq_along(args)) {

> name = args.names[[i]]

> if (substr(name, 1, 1) == '.') {

> force(E[[name]])

> }

> }

> })()

>

> environment()

> }

>

> args = process.args(as.list(substitute(list(...)))[-1L])

> args.names = names(args)

> formals(foo) = args

>

> foo

> }

> B = item(

> x =,

> y =,

>

> grab(A(x, y))

> )

> b = B(2, 3)

> as.list(b)

$sum
[1] 5

$x
[1] 2

$y
[1] 3

Perfect.

To leave a comment for the author, please follow the link and comment on his blog: Struggling Through Problems.

R-bloggers.com offers daily e-mail updates about R news and tutorials on topics such as: visualization (ggplot2, Boxplots, maps, animation), programming (RStudio, Sweave, LaTeX, SQL, Eclipse, git, hadoop, Web Scraping) statistics (regression, PCA, time series, trading) and more...



If you got this far, why not subscribe for updates from the site? Choose your flavor: e-mail, twitter, RSS, or facebook...

Comments are closed.