dispatching S4 methods with an expression as argument
Asked Answered
O

2

11

I'm trying to convince an S4 method to use an expression as an argument, but I always get an error returned. A trivial example that illustrates a bit what I'm trying to do here :

setGeneric('myfun',function(x,y)standardGeneric('myfun'))

setMethod('myfun',c('data.frame','expression'),
          function(x,y) transform(x,y) )

If I now try :

> myfun(iris,NewVar=Petal.Width*Petal.Length)
Error in myfun(iris, NewVar = Petal.Width * Petal.Length) : 
  unused argument(s) (NewVar = Petal.Width * Petal.Length)

> myfun(iris,{NewVar=Petal.Width*Petal.Length})
Error in myfun(iris, list(NewVar = Petal.Width * Petal.Length)) : 
 error in evaluating the argument 'y' in selecting a method for 
 function 'myfun': Error: object 'Petal.Width' not found

It seems the arguments are evaluated in the generic already if I understand it right. So passing expressions down to methods seems at least tricky. Is there a possibility to use S4 dispatching methods using expressions?


edit : changed to transform, as it is a better example.

Overlive answered 9/8, 2011 at 16:47 Comment(6)
See pg 399-402 of SfDA where Chambers describes why any signature-tested argument must be evaluated.Exponible
@DWin : care to share a link? sfDA sounds unfamiliar to me, and I reckon you're not talking about the State Food and Drug Administration...Overlive
Sorry. "Software for Data Analysis" by John Chambers. As an additional point I noticed that your signature values were not associated with argument names, whereas in the examples that is the format. (Note: you have not changed to transform in the question body yet.)Exponible
I think you're confusing the two uses of expression - the object created by expression (which you can dispatch) vs. capturing the unevaluated promise from lazy argument expression (which you can't dispatch on)Workhorse
@Workhorse : thx for taking a look. I did lot of experimenting with expression() and as.expression() didn't really help me further alas. Tried to get an expression from the match.call(), but also there I didn't get any further. Care to elaborate a bit further on the difference between both uses of expression?Overlive
What are you trying to do? It would be easier to answer your question with a bit of context.Workhorse
S
2

You've specified "expression" as the class of the second argument in this example method. The first example returns an error because

NewVar=Petal.Width*Petal.Length

is being interpreted as a named argument to myfun with value

Petal.Width*Petal.Length

that doesn't get the chance to be evaluated, because NewVar isn't an argument for this method or generic.

In the second example, I'm not sure what is going on with the closed curly braces, as my error differs from the one shown:

Error in myfun(iris, { : error in evaluating the argument 'y' in selecting a method for function 'myfun': Error: object 'Petal.Width' not found

However, I receive no error and get the iris data.frame as output when I force your expression to be an expression object:

myfun(iris, expression(NewVar=Petal.Width*Petal.Length))

I think this only partially answers your question, because trivially returning the iris data.frame was not what you wanted. The expression is not being evaluated properly by transform(). I suspect you want the output to match exactly the output from the following hard-coded version:

transform(iris, NewVar=Petal.Width*Petal.Length)

Here is a short example evaluating the expression using eval

z <- expression(NewVar = Petal.Width*Petal.Length)
test <- eval(z, iris)
head(test, 2)

[1] 0.28 0.28

Here is a version that works for adding one variable column to the data.frame:

setGeneric('myfun',function(x,y)standardGeneric('myfun'))
setMethod('myfun',c('data.frame','expression'), function(x,y){
    etext <- paste("transform(x, ", names(y), "=", as.character(y), ")", sep="")
    eval(parse(text=etext))
})
## now try it.
test <- myfun(iris, expression(NewVar=Petal.Width*Petal.Length))
names(test)

[1] "Sepal.Length" "Sepal.Width" "Petal.Length" "Petal.Width" "Species" "NewVar"

head(test)

    Sepal.Length Sepal.Width Petal.Length Petal.Width Species NewVar
1          5.1         3.5          1.4         0.2  setosa   0.28
2          4.9         3.0          1.4         0.2  setosa   0.28

Again, this implementation has essentially hard-coded that one, and only one, variable column will be added to the input data.frame, although the name of that variable column and the expression are arbitrary, and provided as an expression. I'm certain there is a better, more general answer that would evaluate the expression held in y as if it were a direct call in the transform() function, but I'm stumped at the moment what to use as the appropriate "inverse" function to expression( ).

There is always the standard ... , if you don't actually want to dispatch on y:

setGeneric('myfun', function(x, ...) standardGeneric('myfun'))
setMethod('myfun', 'data.frame', function(x, ...){
    transform(x, ...)
})

And this works great. But your question was about actually dispatching on an expression object.

The following does not work, but I think it is getting closer. Perhaps someone can jump in and make some final tweaks:

setGeneric('myfun', function(x, y) standardGeneric('myfun'))
setMethod('myfun',c('data.frame', 'expression'), function(x, y){
    transform(x, eval(y, x, parent.frame()))
})
## try out the new method
z <- expression(NewVar = Petal.Width*Petal.Length)
test <- myfun(iris, z)
names(test)

[1] "Sepal.Length" "Sepal.Width" "Petal.Length" "Petal.Width" "Species"

Essentially, the "NewVar=" piece of the expression was not passed to transform() when we called myfun().

After much trial and error, I figured out a way that works for real. First convert the expression object into a list with as.list(), then build up the call that you want with the marvelous

do.call()

The complete example looks like this:

setGeneric('myfun', function(x, y) standardGeneric('myfun'))
setMethod('myfun',c('data.frame', 'expression'), function(x, y){
    do.call("transform", c(list(x), as.list(y)))
})
# try out the new method
z <- expression(NewVar = Petal.Width*Petal.Length)
test <- myfun(iris, z)
names(test)
[1] "Sepal.Length" "Sepal.Width"  "Petal.Length" "Petal.Width"  "Species"     
[6] "NewVar"

And the new data.frame object "test" has the "NewVar" column we wanted.

Soniasonic answered 10/9, 2011 at 8:41 Comment(2)
eval poses a whole new set of problems when you have nested functions. Environments... I know I can hardcode it, but the point is essentially to have the flexibility of transform but for my own S4 objects. I solved it using S3 methods, but I was especially interested in what was going on behind the screens...Overlive
Yes, I was finding the same problem with eval. I think the do.call approach I appended might work for your problem.Payton
F
1

It's not S4, or the argument evaluation, its that R can't tell if you mean to pass a named parameter or if your expression is of the form a=b.

If you look at the help for "within", it uses curly brackets to make sure the expression is parsed as an expression.

I also think calling within inside a function won't do the replacement in the function's caller...

I also think I don't know enough about expressions.

Flavopurpurin answered 9/8, 2011 at 17:8 Comment(5)
Aren't the braces only because the expr is a multi-line expression in the sense of multiple expressions? You don't need braces in within() if you supply a single R expression.Greenwich
Your point regarding named parameter or expression is interesting. Is this a time where the inequality of = and <- as assignment is biting @Joris in the ass? IIRC you must use <- in within(), as = doesn't work as assignment there.Greenwich
@Gavin : I agree, within was a stupid example. I use transform to illustrate, but the problem is really in the dispatching. the expression seems to be evaluated before dispatching happens, and I have no clue how to turn that off...Overlive
Have you tried enclosing it in curly braces? That would probably prevent similar 'parsing errors'? May not solve your problem but could shed light.Scrimpy
@Nick : I did. and the light it sheds is what DWin says : it gets evaluated at dispatching. I tried extracting it using match.call(), but then I can't get it into transform.Overlive

© 2022 - 2024 — McMap. All rights reserved.