tblznbits
tblznbits

Reputation: 6778

Which environment should be called when using eval( ) in a function?

I've got a set of functions that I'm trying to work with and I'm struggling to figure out why the assignment isn't working. Here are the functions I'm using:

new_timeline <- function() {
  timeline = structure(list(), class="timeline")

  timeline$title <- list("text" = list("headline" = NULL, "text" = NULL),
                         "start_date" = list("year" = NULL, "month" = NULL, "day" = NULL),
                         "end_date" = list("year" = NULL, "month" = NULL, "day" = NULL))

  return(timeline)
}

.add_date <- function(self, date, time_type) {
  valid_date <- stringr::str_detect(date, "^[0-9]{4}(-[0-9]{1,2}){0,2}$")
  if (!valid_date) {
    stringr::str_interp("Your ${time_type} date does not appear to be formatted correctly. It must be of the form 'yyyy-mm-dd'. Only the year is required.") %>% stop()
  }

  date_elements <- date %>% as.character() %>% stringr::str_split(" ") %>% unlist()
  date <- date_elements[1] %>% stringr::str_split("-") %>% unlist()
  stringr::str_interp("self$title$${time_type}_date$year <- date[1]") %>% parse(text = .) %>% eval()
  if (!is.na(date[2])) stringr::str_interp("self$title$${time_type}_date$month <- date[2]") %>% parse(text = .) %>% eval()
  if (!is.na(date[3])) stringr::str_interp("self$title$${time_type}_date$day <- date[3]") %>% parse(text = .) %>% eval()

  return(self)
}

edit_title <- function(self, headline = NULL, text = NULL, start_date = NULL, end_date = NULL) {

  if (class(self) != "timeline") stop("The object passed must be a timeline object.")
  if (is.null(headline) && is.null(self$title$text$headline)) stop("Headline cannot be empty when adding a new title.")

  if (!is.null(headline)) self$title$text$headline <- headline
  if (!is.null(text)) self$title$text$text <- text
  if (!is.null(start_date)) self <- .add_date(self, date = start_date, time_type = "start")
  if (!is.null(end_date)) self <- .add_date(self, date = end_date, time_type = "end")

  return(self)
}

EDIT: The above code has been severely reduced per a request in the comments. The code is still sufficient to reproduce the error.

I know that's a bit long-winded, so I apologize. The first function establishes a new timeline object. The third function allows us to change the title of the timeline object and the second function is a helper function that handles dates. The code would be used like this:

library(magrittr)
#devtools::install_github("hadley/stringr")
library(stringr)

tl <- new_timeline()
tl <- tl %>% edit_title(headline = "My Timeline", text = "Example", start_date = "2015-10-18")

The code runs with no errors, but when I call tl$title$start_date$year, it comes back as NULL. Using an answer I got in this previous question I asked, I tried to set envir = globalenv() within the eval function. When I do that, the function returns an error saying that object self cannot be found.

So I'm under the impression that self is held in the parent.frame(). So I add both of these to a list: envir = list(globalenv(), parent.frame()). This causes the function to run without error, but there's still no assignment.

Where am I going wrong? Thanks in advance!

Upvotes: 1

Views: 126

Answers (1)

Rorschach
Rorschach

Reputation: 32416

As mentioned in the comments, I think you could probably do away with all of the code parsing and just pass variables in [[ for your assignments. Anyway, when you use the pipe operator a bunch of function wrapping happens so determining how many frames to go back is painful. Here are a couple solutions modifying the .add_date function.

You already found one, using <<-, since it searches back through the parent environments until it finds the variable (or doesnt and assigns it in the global).

Another would be just storing the function environment() and passing that to eval.

A third would be counting how many frames deep you go, and using sys.frame to tell eval which environment to look in.

.add_date <- function(self, date, time_type) {
    valid_date <- stringr::str_detect(date, "^[0-9]{4}(-[0-9]{1,2}){0,2}$")
    if (!valid_date) {
        stringr::str_interp("Your ${time_type} date does not appear to be formatted correctly. It must be of the form 'yyyy-mm-dd'. Only the year is required.") %>% stop()
    }

    ## Examining environemnts
    e <- environment()                                                              # current env
    efirst <- sys.nframe()                                                          # frame number
    print(paste("Currently in frame", efirst))
    envs <- stringr::str_interp("${date}") %>% parse(text=.) %>% {.; sys.frames()}  # list of frames
    elast <- stringr::str_interp("${date}") %>% parse(text=.) %>% {.; sys.nframe()} # number of last
    print(paste("Went", elast, "frames deep."))

    ## Go back this many frames in eval
    goback <- efirst-elast

    date_elements <- date %>% as.character() %>% stringr::str_split(" ") %>% unlist()
    date <- date_elements[1] %>% stringr::str_split("-") %>% unlist()

    ## Solution 1: use sys.frame
    stringr::str_interp("self$title$${time_type}_date$year <- date[1]") %>%
      parse(text = .) %>% eval(envir=sys.frame(goback)) 

    ## Solution 2: use environment defined in function
    if (!is.na(date[2])) stringr::str_interp("self$title$${time_type}_date$month <- date[2]") %>%
      parse(text = .) %>% eval(envir=e)

    return(self)
}

Upvotes: 1

Related Questions