Avoiding consideration of enclosing frames when retrieving field value of a S4 Reference Class
Asked Answered
R

1

8

I'm a huge fan of S4 Reference Classes as they allow for a hybrid programming style (functional/pass-by-value vs. oop/pass-by-reference; example) and thus increase flexibility dramatically.

However, I think I just came across an undesired behavior with respect to the way R scans through environments/frames when you ask it to retrieve a certain field value via method $field() (see help page). The problem is that R also seems to look in enclosing environments/frames if the desired field is not found in the actual local/target environment (which would be the environment making up the S4 Reference Class), i.e. it's just like running get(<objname>, inherits=TRUE) (see help page).

Actual question

In order to have R just look in the local/target environment, I was thinking something like $field(name="<fieldname>", inherits=FALSE) but $field() doesn't have a ... argument that would allow me to pass inherits=FALSE along to get() (which I'm guessing is called somewhere along the way). Is there a workaround to this?


Code Example

For those interested in more details: here's a little code example illustrating the behavior

setRefClass("A", fields=list(a="character"))

x <- getRefClass("A")$new(a="a")

There is a field a in class A, so it's found in the target environment and the value is returned:

> x$field("a")
[1] "a"

Things look differently if we try to access a field that is not a field of the reference class but happens to have a name identical to that of some other object in the workspace/searchpath (in this case "lm"):

require("MASS")
> x$field("lm")

function (formula, data, subset, weights, na.action, method = "qr", 
    model = TRUE, x = FALSE, y = FALSE, qr = TRUE, singular.ok = TRUE, 
    contrasts = NULL, offset, ...) 
{
    ret.x <- x
    ret.y <- y

    [omitted]

    if (!qr) 
        z$qr <- NULL
    z
}
<bytecode: 0x02e6b654>
<environment: namespace:stats>

Not really what I would expect at this point. IMHO an error or at least a warning would be much better. Or opening method $field() for arguments that can be passed along to other functions via .... I'm guessing somewhere along the way get() is called when calling $field(), so something like this could prevent the above behavior from occurring:

x$field("digest", inherits=FALSE)

Workaround: own proposal

This should do the trick, but maybe there's something more elegant that doesn't involve the specification of a new method on top of $field():

setRefClass("A", fields=list(a="character"),
    methods=list(
        myField=function(name, ...) {
            # VALIDATE NAME //
            if (!name %in% names(getRefClass(class(.self))$fields())) {
                stop(paste0("Invalid field name: '", name, "'"))
            }
            # //
            .self$field(name=name)
        }
    )
)
x <- getRefClass("A")$new(a="a")

> x$myField("a")
[1] "a"
> x$myField("lm")
Error in x$myField("lm") : Invalid field name: 'lm'
Regent answered 20/3, 2013 at 16:27 Comment(4)
I think the general approach is i) pose question, ii) wait for Martin Morgan to come around and answer ;-)Watercolor
@DirkEddelbuettel: hahaha... good one ;-)Regent
As an aside, you could use require(MASS) and lm in your example (so that it doesn't require external packages).Pani
@csgillespie: good point, thanks!Regent
L
1

The default field() method can be replaced with your own. So adding an inherits argument to avoid the enclosing frames is simply a matter of grabbing the existing x$field definition and adding it...

setRefClass( Class="B",
             fields= list( a="character" ),
             methods= list(
               field = function(name, value, inherits=TRUE ) {
                 if( missing(value) ) {
                   get( name, envir=.self, inherits=inherits )
                 } else {
                   if( is.na( match( name, names( .refClassDef@fieldClasses ) ) ) ) {
                     stop(gettextf("%s is not a field in this class", sQuote(name)), domain = NA)
                   }
                   assign(name, value, envir = .self)
                 }
               }
             ),
)

Or you could have a nice error message with a little rearranging

setRefClass( Class="C",
             fields= list( a="character" ),
             methods= list(
               field = function(name, value, inherits=TRUE ) {
                if( is.na( match( name, names( .refClassDef@fieldClasses ) ) ) &&
                      ( !missing(value) || inherits==FALSE) ) {
                  stop(gettextf("%s is not a field in this class", sQuote(name)), domain = NA)
                }

                if( missing(value) ) {
                    get( name, envir=.self, inherits=inherits )
                } else {
                  assign(name, value, envir = .self)
                }
               }
             ),
)

Since you can define any of your own methods to replace the defaults pretty much any logic you want can be implemented for your refclasses. Perhaps an error if the variable is acquired using inheritance but the mode matches to c("expression", "name", "symbol", "function") and warning if it doesn't directly match the local refClass field names?

Lardon answered 16/6, 2013 at 23:8 Comment(1)
That looks very promising at first a glance! Thanks a lot!!Regent

© 2022 - 2024 — McMap. All rights reserved.