Menu Home

unpack Your Values in R

I would like to introduce an exciting feature in the upcoming 1.9.6 version of the wrapr R package: value unpacking.

The unpacking notation is made available if you install wrapr version 1.9.6 from Github:

remotes::install_github("WinVector/wrapr")

We will likely send this version to CRAN in a couple of weeks.

Here is an example of the unpack feature in use. First we set up some example data.

library(wrapr)
packageVersion('wrapr')
#> [1] '1.9.6'

# make some example data
d <- data.frame(
  x = 1:2,
  g = c('test', 'train'),
  stringsAsFactors = FALSE)

Now we demonstrate the new feature: unpacking a named-list returned from a function. In this case we will demonstrate the effect using the function base::split(). base::split() splits data into a named list, with the names coming from the grouping vector. Our unpack feature will conveniently assign these sub-dataframes into our environment for us.

# unpack the data into our workspace
# notation is assignment-like: NEW_VARAIBLE = NAMED_ITEM
unpack[train_set = train, test_set = test] <- split(d, d$g)

In the above example base::split() built a named list of sub-dataframes from our original data frame d. We used unpack[] to assign these named items into our working environment as the new variables: train_set and test_set. The unpacking was triggered by assigning the split results into the special unpack[] notation. Notice the unpack specification itself also looks like assignments (or more precisely argument bindings) with new names (used to say where values will be written) on the left and old names (used to say where values are found) on the right.

We can confirm that the training data is in the train_set variable, and the test data is in the test_set variable:

# confirm we have the new variables

print(train_set)
#>   x     g
#> 2 2 train

print(test_set)
#>   x    g
#> 1 1 test

The unpacking notation, when used in this manner, doesn’t depend on the order of the values. This makes for very safe code that concisely documents intent.

There is a small side effect, due to R’s assignment rules using the []<- notation will write a valued named “unpack” into the working environment. If one wishes to avoid this they can use either a function notation:

unpack(split(d, d$g), train_set = train, test_set = test) 

Or a “pipe into function” notation:

split(d, d$g) %.>% unpack(., train_set = train, test_set = test) 

(Note: the above was the wrapr dot-pipe. Currently unpack() does not work with the margrittr pipe, as in that case the results appear to get written into a temporary intermediate environment created by magrittr, and then lost. This difference between pipes isn’t so much a problem with unpack, but the extent that the wrapr dot pipe is designed for user extension.)

A killer application of unpack is: replacing save(a, b, file = FNAME)/load(file = FNAME) with a much safer and more explicit saveRDS(list(a = a, b = b, ...), file = FNAME)/unpack(readRDS(file = FNAME), a, b, ...) pattern. Here we are using the convention that names alone such as “a” implicitly stand for “a = a“. We now have some notes on this idea here.

We are still working on choosing names for this function. Likely we will pick “unpack” for the functional form, and perhaps one of “to” or “into” for the array-bracket assignment form. Right now we implement all 3 names, each with all functionality.

If you don’t want to bring in all of wrapr with library(wrapr), you can bring in just a few bits as follows:

unpack <- wrapr::unpack
`%.>%` <- wrapr::`%.>%`

We are designing unpack to be very strict in its name checking before writing any values to the workspace. This is to avoid partial assignments where some fields are written and others are missing.

We hope you check out the unpack feature and use it to your projects.

Related work includes:

  • The zeallot::%<-% arrow already supplies excellent positional or ordered unpacking. But we feel that style may be more appropriate in the Python world where many functions return un-named tuples of results. Python functions tend to have positional tuple return values because the Python language has had positional tuple unpacking as a core language feature for a very long time (thus positional structures have become “Pythonic”). R has not emphasized positional unpacking, so R functions tend to return named lists or named structures. For named lists or named structures it may not be safe to rely on value positions. So I feel it is more “R-like” to use named unpacking.
  • vadr::bind supplies named unpacking, but appears to use a “SOURCE = DESTINATION” notation. That is the reverse of a “DESTINATION = SOURCE” which is how both R assignments and argument binding are already written.
  • base::attach. base::attach adds items to the search path with names controlled by the object being attached (instead of by the user).
  • base::with(). unpack(list(a = 1, b = 2), x <- a, y <- b)
    works a lot like
    with(list(a = 1, b = 2), { x <<- a; y <<-b })
    .
  • wrapr::let(). wrapr::let() re-maps names during code execution using a “TARGET = NEWNAME” target replacement scheme, where TARGET acts as if it had the name stored in NEWNAME for the duration of the let-block.

Categories: Exciting Techniques Tutorials

Tagged as:

jmount

Data Scientist and trainer at Win Vector LLC. One of the authors of Practical Data Science with R.

%d bloggers like this: