Using column name as function argument within a S3 methods

I'm trying to learn S3 by using a single function which will create different plots depending on the class of the aes(x) supplied - if x is a factor then geom_boxplot plot, if it's numeric then geom_point

library(ggplot2)
library(glue)
library(rlang)

myplot <- function(data, x, y) {
  UseMethod("myplot", x)
}

# the default myplot behavior
# we're going to define what to do when x isn't a numeric or factor
# print error: x column is of class ____
myplot.default <- function(data, x, y) {
  abort(glue(
    "Can't use myplot because {deparse(substitute(x))} is of type ",
    glue_collapse(class(x), "/")
  ))
}

# when x is a factor
myplot.factor <- function(data, x, y ) {
  ggplot(data, aes(x = x, y = y)) +
    geom_boxplot()
}

# when x is a numeric
myplot.numeric <- function(data, x, y) {
  ggplot(data, aes(x = x, y = y)) +
    geom_point()
}

And this works:

myplot(iris, iris$Species, iris$Sepal.Length)
myplot(iris, iris$Sepal.Width, iris$Sepal.Length)

# test that character will return default behavior
# as expected trying to run this on a character
# results in the default behavior
iris2 <- mutate_if(iris, is.factor, as.character)
myplot(iris2, iris2$Species, iris2$Sepal.Length)

But I want this to work:

myplot(iris, Species, Sepal.Length)

I've tried endless combinations of quo enquo deparse get - you name it but I still have gaps in my rlang understanding. Any help appreciated!!

1 Like

Just bumped against the same issue myself and made a note to look into it. Will come back when/if I do. Thanks for the added impetus.

Hi @MayaGans; I'm not sure I understand what you're hoping for -- did you want myplot to work with column names for x and y, or vectors (as in your working examples), or both?

Hi @dromano I want it to work with the column names, but only got it to work in my example using vectors - is that doable [where the methods would be looking for the class of the first column provided]? Thanks for bearing with me, I'm just learning S3 and thought this could be a cool example but have no idea how realistic it is - any lack of clarity is a result of my fuzzy mental model so honing in on the question is really helpful!

1 Like

I don't know much about S3, but have been running into issues with names, so I thought I'd try things that have worked for me before. I'm sure this is hacky, but it seems to work, although I haven't tested it further than you see here:

library(ggplot2)
library(glue)
library(rlang)

myplot <- function(data, x, y) {
  if(is.call(enexpr(x))){
    UseMethod("myplot", x)
  } else {
    x <- enexpr(x)
    UseMethod("myplot", data[[as_name(x)]])
  }
}

# the default myplot behavior
# we're going to define what to do when x isn't a numeric or factor
# print error: x column is of class ____
myplot.default <- function(data, x, y) {
  if(is.symbol(enexpr(x))){
    x <- enexpr(x)
    abort(glue(
      "Can't use myplot because {deparse(substitute(x))} is of type ",
      glue_collapse(class(data[[as_name(x)]]), "/")
    ))
  } else {
  }
  abort(glue(
    "Can't use myplot because {deparse(substitute(x))} is of type ",
    glue_collapse(class(x), "/")
  ))
}

# when x is a factor
myplot.factor <- function(data, x, y ) {
  if(is.symbol(enexpr(x))){
  x <- enexpr(x)
  } else {
    x <- substitute(x)
  }
  y <- enexpr(y)
  ggplot(data, aes(x = !!x, y = !!y)) +
    geom_boxplot()
}

# when x is a numeric
myplot.numeric <- function(data, x, y) {
  if(is.symbol(enexpr(x))){
    x <- enexpr(x)
  } else {
    x <- substitute(x)
  }
  y <- enexpr(y)
  ggplot(data, aes(x = !!x, y = !!y)) +
    geom_point()
}

myplot(iris, iris$Species, iris$Sepal.Length)
myplot(iris, Species, iris$Sepal.Length)
myplot(iris, iris$Species, Sepal.Length)

myplot(iris, iris$Sepal.Width, iris$Sepal.Length)
myplot(iris, Species, iris$Sepal.Length)
myplot(iris, iris$Species, Sepal.Length)

# test that character will return default behavior
# as expected trying to run this on a character
# results in the default behavior
iris2 <- mutate_if(iris, is.factor, as.character)
myplot(iris2, iris2$Species, iris2$Sepal.Length)
myplot(iris2, Species, iris2$Sepal.Length)

I'm not sure this helps you understand S3, but hopefully it helps some in terms of working with names.

1 Like

Ah ha! So just to make sure I understand, I wanted to reiterate what you're doing in words if that's okay, @dromano?

enexpr lets you use the column name Species as an argument because "it returns the R expression describing how to make the value" [not exactly sure what that means but I do get that it doesn't try to return Species which doesn't exist and was my error before]

Then with this new enexpr-ed Species we can use !! to force evaluation within the ggplot call?

I'm not used to this myself, but I think the way to think about this is that functions like enexpr() freeze the evaluation of the implicit expression being passed to them, and using !! triggers the completion of the evaluation.

I've seen the term 'promise' used in this context, but hadn't realized it had the connotation of 'unfinished' until I tried to walk through your code with debug(): I couldn't seem to capture the expression iris$Species because as soon as I inspected the value of x, it turned into a data frame (which reminded me a litlle of Schrodinger's cat). And when I tried to inspect !!x or !!y when I was walking through my version of your code with debug(), the graph that was produced showed x or y instead of the name (depending on what I had inspected), even though the code was identical to what I posted!

I would recommend walking through my code with debug() and inspecting the values that are passed between the functions at each step -- it was very enlightening for me.

2 Likes

This topic was automatically closed 7 days after the last reply. New replies are no longer allowed.