tidyverse/design

Initiatialising a variable on package load

jennybc opened this issue · 15 comments

Summarizing a recent Slack discussion that seems worth preserving.

The conversation was triggered by r-lib/gargle#184, where someone following a pattern seen in several gargle-using packages was getting push back from CRAN for a new package submission.

Specifically, CRAN objected to the use of <<- in .onLoad(), interpreting that as modifying the global environment.
As it turns out, this object was created elsewhere in the package, in the package's namespace, so the <<- was modifying that object.

But this use of <<- is not easily seen to be OK, so it's worth having better patterns for this.

Typical "before" code, that might get flagged by CRAN:

# in R/drive_auth.R
.auth <- NULL

# in R/zzz.R
.onLoad <- function(libname, pkgname) {

  .auth <<- gargle::init_AuthState(
    package     = "googledrive",
    auth_active = TRUE
  )
 
 ...
}

The objective in this case is to make sure that googledrive's internal .auth object is an instance of gargle's R6 class AuthState, according to the current, locally installed version of gargle, not the ambient gargle version when the googledrive binary was built.

Discussion with @jimhester @gaborcsardi @jeroen @DavisVaughan lead to these observations and alternatives:

  • Put the .auth <- NULL assignment as close as possible to the <<-, so it's easier to see what's going on and that the super assignment does not touch global env.

  • Use utils::assignInMyNamespace() to make it clear that the target object lives in my namespace. You still must create the target object elsewhere, as utils::assignInMyNamespace() can only modify, not create.

    # in R/drive_auth.R or R/zzz.R (each has some pros/cons)
    .auth <- NULL
    
    # in R/zzz.R
    .onLoad <- function(libname, pkgname) {
        utils::assignInMyNamespace(
          ".auth",
          gargle::init_AuthState(package = "googledrive", auth_active = TRUE)
        )
    }
  • Use assign() and specify the environment as environment(.onLoad), topenv(), or asNamespace("pkg"). Note that the target .auth object does not need to be pre-created in this case.

    .onLoad <- function(libname, pkgname) {
        assign(
          ".auth",
          gargle::init_AuthState(package = "googledrive", auth_active = TRUE),
          environment(.onLoad)
        )
    }

Another way is to hook assignment like this:

on_load(
  var <- value
)

This uses the hook implemented in https://github.com/r-lib/rlang/blob/master/R/aaa.R (could be made a base-only compat file and I'm planning to export from rlang at some point). The big advantage besides readability is that you can put the assignment in context rather than in .onLoad(). Example: https://github.com/r-lib/rlang/blob/dc03e447/R/env-special.R#L389-L392. I also use it for things like lazy S3 registration: https://github.com/r-lib/rlang/blob/dc03e447/R/quo.R#L257-L267