withr 2.1.0

  r-lib, withr

  Jim Hester

withr 2.1.0 is now available on CRAN! withr makes working with global state in R safer and less error prone. It has only base package dependencies so is easily included in packages.

Install the latest version with:

install.packages("withr")

Whither withr?

Many functions in R modify global state in some fashion. Some common examples are par() for graphics parameters, dir() to change the current directory and options() to set a global option. Using these functions is handy when using R interactively, because you can set them early in your experimentation and they will remain set for the duration of the session. However this makes programming with these settings difficult, because they make your function impure by modifying a global state. Therefore you should always strive to reset the previous state when the function exists.

One common idiom for dealing with this problem is to save the current state, make your change, then restore the previous state.

par("col" = "black")
my_plot <- function(new) {
  old <- par(col = "red", pch = 19)
  plot(mtcars$hp, mtcars$wt)
  par(old)
}
my_plot()

par("col")
#> [1] "black"

However this approach can fail if there’s an error before you are able to reset the options.

par("col" = "black")
my_plot <- function(new) {
  old <- par(col = "red", pch = 19)
  plot(mtcars$hpp, mtcars$wt)
  par(old)
}
my_plot()
#> Error in xy.coords(x, y, xlabel, ylabel, log): 'x' and 'y' lengths differ
par("col")
#> [1] "red"

Using the base function on.exit() is a robust solution to this problem. on.exit() will run the code when the function is exited, regardless whether it exits normally or with an error.

par("col" = "black")
my_plot <- function(new) {
  old <- par(col = "red", pch = 19)
  on.exit(par(old))
  plot(mtcars$hpp, mtcars$wt)
}
my_plot()
#> Error in xy.coords(x, y, xlabel, ylabel, log): 'x' and 'y' lengths differ
par("col")
#> [1] "black"

options(test = 1)
{
  print(getOption("test"))
  on.exit(options(test = 2))
}
#> [1] 1
getOption("test")
#> [1] 2

However this solution is somewhat cumbersome to work with. You need to remember to use an on.exit() call after each stateful call. In addition by default each on.exit() action will overwrite any previous on.exit() action in the same function unless you use the add = TRUE option. add = TRUE also adds additional code to the end of existing code, which means the code is not run in the Last-In, First-Out order you would generally prefer. It is also not possible to have this cleanup code performed before the function has finished.

withr is a solution to these issues. It defines a large set of functions for dealing with global settings in R, such as with_par(). These functions set one of the global settings for the duration of a block of code, then automatically reset it after the block is completed.

par("col" = "black")
my_plot <- function(new) {
  with_par(list(col = "red", pch = 19),
    plot(mtcars$hp, mtcars$wt)
  )
  par("col")
}
my_plot()

#> [1] "black"
par("col")
#> [1] "black"

In addition to the with_* functions there are local_* variants whose effects last until the end of the function they are included in. These work similar to on.exit(), but you can set the options in one call rather than two.

par("col" = "black")
my_plot <- function(new) {
  local_par(list(col = "red", pch = 19))
  plot(mtcars$hp, mtcars$wt)
}
my_plot()

par("col")
#> [1] "black"

New features

Here are some highlights of new functions for v2.1.0.

Graphics devices

There are now a comprehensive set of functions to deal with R’s builtin graphics devices.

These functions open a new graphics device, run some code, then automatically close the device.

path <- file.path(Sys.glob("*withr*_files/figure-html"), "test.png")
with_png(path, width = 400, height = 300, {
  plot(mtcars$hp, mtcars$mpg)
})

Thanks to Richard Cotton’s great pull request for this feature!

Connections

There are two new functions for cleaning up connections in R. with_connection() allows you to automatically close R’s file connections. Here we create a writable file connection, write some lines to it with with_connection(), then open a read-only connection and read the file using local_connection().

with_connection(list(con = file("temp", "w")), {
  writeLines(c("foo", "bar"), con)
})
read_temp <- function() {
  con <- local_connection(file("temp", "r"))
  readLines(con)
}
read_temp()
#> [1] "foo" "bar"

with_db_connection() provides DBI connections to databases which automatically call DBI::dbDisconnect(). Here we create a new SQLite database, connect to it with with_db_connection(), and write a new table to it. We then create another connection with local_db_connection() and read from the table.

db <- tempfile()
with_db_connection(
  list(con = DBI::dbConnect(RSQLite::SQLite(), db)), {
    DBI::dbWriteTable(con, "mtcars", mtcars)
})
#> Loading required namespace: DBI

head_db_table <- function(...) {
  con <- local_db_connection(DBI::dbConnect(RSQLite::SQLite(), db))
  head(DBI::dbReadTable(con, "mtcars"), ...)
}
head_db_table()
#>    mpg cyl disp  hp drat    wt  qsec vs am gear carb
#> 1 21.0   6  160 110 3.90 2.620 16.46  0  1    4    4
#> 2 21.0   6  160 110 3.90 2.875 17.02  0  1    4    4
#> 3 22.8   4  108  93 3.85 2.320 18.61  1  1    4    1
#> 4 21.4   6  258 110 3.08 3.215 19.44  1  0    3    1
#> 5 18.7   8  360 175 3.15 3.440 17.02  0  0    3    2
#> 6 18.1   6  225 105 2.76 3.460 20.22  1  0    3    1
unlink(db)

Packages

with_package() allows you to temporarily attach a package.

with_package("lattice", {
  xyplot(y ~ x, data.frame(x = -2:2, y = dnorm(-2:2)))
})

Tempfiles

with_tempfile() handy for creating a new temporary files that are removed, often useful when writing tests.

with_tempfile("file1", {
  print(file1)
  writeLines("foo", file1)
  readLines(file1)
})
#> [1] "/var/folders/dt/r5s12t392tb5sk181j3gs4zw0000gn/T//RtmpC6vpTp/file15b492f945411"
#> [1] "foo"

Other changes

There are many other bug fixes and other minor improvements in this release. You can see a complete list in the release notes.

A big thanks goes to all the community members who contributed code and opened issues since the last release!

@QuLogic, @krlmlr, @hadley, @wlandau-lilly, @jimhester, @kevinushey, and @richierocks