The zeallot package defines an operator for unpacking
assignment, sometimes called parallel assignment or
destructuring assignment in other programming languages. The
operator is written as %<-%
and used like this.
The result is that the list is unpacked into its elements, and the
elements are assigned to lat
and lng
.
You can also unpack the elements of a vector.
You can unpack much longer structures, too, of course, such as the 6-part summary of a vector.
c(min_wt, q1_wt, med_wt, mean_wt, q3_wt, max_wt) %<-% summary(mtcars$wt)
min_wt
#> [1] 1.513
q1_wt
#> [1] 2.58125
med_wt
#> [1] 3.325
mean_wt
#> [1] 3.21725
q3_wt
#> [1] 3.61
max_wt
#> [1] 5.424
If the left-hand side and right-hand sides do not match, an error is raised. This guards against missing or unexpected values.
c(stg1, stg2, stg3) %<-% list("Moe", "Donald")
#> Error in c(stg1, stg2, stg3) %<-% list("Moe", "Donald"): missing value for variable `stg3`
A common use-case is when a function returns a list of values and you
want to extract the individual values. In this example, the list of
values returned by coords_list()
is unpacked into the
variables lat
and lng
.
#
# A function which returns a list of 2 numeric values.
#
coords_list <- function() {
list(38.061944, -122.643889)
}
c(lat, lng) %<-% coords_list()
lat
#> [1] 38.06194
lng
#> [1] -122.6439
In this next example, we call a function that returns a vector.
You can directly unpack the coefficients of a simple linear regression into the intercept and slope.
safely
The purrr package includes the safely
function.
It wraps a given function to create a new, “safe” version of the
original function.
The safe version returns a list of two items. The first item is the
result of calling the original function, assuming no error occurred; or
NULL
if an error did occur. The second item is the error,
if an error occurred; or NULL
if no error occurred. Whether
or not the original function would have thrown an error, the safe
version will never throw an error.
pair <- safe_log("donald")
pair$result
#> NULL
pair$error
#> <simpleError in .Primitive("log")(x, base): non-numeric argument to mathematical function>
You can tighten and clarify calls to the safe function by using
%<-%
.
A data frame is simply a list of columns, so the zeallot assignment does what you expect. It unpacks the data frame into individual columns.
c(mpg=, cyl=, dist=, hp=) %<-% mtcars
cyl
#> [1] 6 6 4 6 8 6 8 4 4 6 6 8 8 8 8 8 8 4 4 4 4 8 8 8 8 4 4 4 8 6 8 4
Bear in mind, a list of data frames is still just a list. The assignment will extract the list elements (which are data frames) but not unpack the data frames themselves.
c(gear3, gear4, gear5) %<-% split(mtcars, ~ gear)
head(gear3)
#> mpg cyl disp hp drat wt qsec vs am gear carb
#> Hornet 4 Drive 21.4 6 258.0 110 3.08 3.215 19.44 1 0 3 1
#> Hornet Sportabout 18.7 8 360.0 175 3.15 3.440 17.02 0 0 3 2
#> Valiant 18.1 6 225.0 105 2.76 3.460 20.22 1 0 3 1
#> Duster 360 14.3 8 360.0 245 3.21 3.570 15.84 0 0 3 4
#> Merc 450SE 16.4 8 275.8 180 3.07 4.070 17.40 0 0 3 3
#> Merc 450SL 17.3 8 275.8 180 3.07 3.730 17.60 0 0 3 3
head(gear4)
#> mpg cyl disp hp drat wt qsec vs am gear carb
#> Mazda RX4 21.0 6 160.0 110 3.90 2.620 16.46 0 1 4 4
#> Mazda RX4 Wag 21.0 6 160.0 110 3.90 2.875 17.02 0 1 4 4
#> Datsun 710 22.8 4 108.0 93 3.85 2.320 18.61 1 1 4 1
#> Merc 240D 24.4 4 146.7 62 3.69 3.190 20.00 1 0 4 2
#> Merc 230 22.8 4 140.8 95 3.92 3.150 22.90 1 0 4 2
#> Merc 280 19.2 6 167.6 123 3.92 3.440 18.30 1 0 4 4
gear5
#> mpg cyl disp hp drat wt qsec vs am gear carb
#> Porsche 914-2 26.0 4 120.3 91 4.43 2.140 16.7 0 1 5 2
#> Lotus Europa 30.4 4 95.1 113 3.77 1.513 16.9 1 1 5 2
#> Ford Pantera L 15.8 8 351.0 264 4.22 3.170 14.5 0 1 5 4
#> Ferrari Dino 19.7 6 145.0 175 3.62 2.770 15.5 0 1 5 6
#> Maserati Bora 15.0 8 301.0 335 3.54 3.570 14.6 0 1 5 8
The %<-%
operator assigned four data frames to four
variables, leaving the data frames intact.
zeallot includes implementations of destructure
for data frames and linear model summaries. However, because
destructure
is a generic function, you can define new
implementations for custom classes. When defining a new implementation
keep in mind the return value must be a list so the values are properly
unpacked.
In some cases, you want the first few elements of a list or vector
but do not care about the trailing elements. The summary.lm
function, for example, returns a list of 11 values, and you may want
only the first few. Fortunately, there is a way to capture those first
few and say “don’t worry about everything else”.
lm_mpg_cyl <- lm(mpg ~ cyl, data = mtcars)
c(lmc_call, lmc_terms, lmc_residuals, ..rest) %<-% summary(lm_mpg_cyl)
lmc_call
#> lm(formula = mpg ~ cyl, data = mtcars)
lmc_terms
#> mpg ~ cyl
#> attr(,"variables")
#> list(mpg, cyl)
#> attr(,"factors")
#> cyl
#> mpg 0
#> cyl 1
#> attr(,"term.labels")
#> [1] "cyl"
#> attr(,"order")
#> [1] 1
#> attr(,"intercept")
#> [1] 1
#> attr(,"response")
#> [1] 1
#> attr(,".Environment")
#> <environment: R_GlobalEnv>
#> attr(,"predvars")
#> list(mpg, cyl)
#> attr(,"dataClasses")
#> mpg cyl
#> "numeric" "numeric"
head(lmc_residuals)
#> Mazda RX4 Mazda RX4 Wag Datsun 710 Hornet 4 Drive
#> 0.3701643 0.3701643 -3.5814159 0.7701643
#> Hornet Sportabout Valiant
#> 3.8217446 -2.5298357
The collector variable rest
captures everything
else.
str(rest)
#> List of 15
#> $ : num 37.9
#> $ : num -2.88
#> $ : num 2.07
#> $ : num 0.322
#> $ : num 18.3
#> $ : num -8.92
#> $ : num 8.37e-18
#> $ : num 6.11e-10
#> $ aliased : Named logi [1:2] FALSE FALSE
#> ..- attr(*, "names")= chr [1:2] "(Intercept)" "cyl"
#> $ sigma : num 3.21
#> $ df : int [1:3] 2 30 2
#> $ r.squared : num 0.726
#> $ adj.r.squared: num 0.717
#> $ fstatistic : Named num [1:3] 79.6 1 30
#> ..- attr(*, "names")= chr [1:3] "value" "numdf" "dendf"
#> $ cov.unscaled : num [1:2, 1:2] 0.4185 -0.0626 -0.0626 0.0101
#> ..- attr(*, "dimnames")=List of 2
#> .. ..$ : chr [1:2] "(Intercept)" "cyl"
#> .. ..$ : chr [1:2] "(Intercept)" "cyl"
Because ..rest
is prefixed with ..
a
variable called rest
is created for the trailing values of
the list.
In addition to collecting trailing values, you can also collect initial values and assign specific remaining values.
c(..skip, e, f) %<-% list(1, 2, 3, 4, 5)
skip
#> [[1]]
#> [1] 1
#>
#> [[2]]
#> [1] 2
#>
#> [[3]]
#> [1] 3
e
#> [1] 4
f
#> [1] 5
Or you can assign the first value, skip values, and then assign the last value.
You can skip one or more values using a period (.
)
instead of a variable name.
You can skip many values with the anonymous collector
(..
).
c(begin, .., end) %<-% list("hello", "blah", list("blah"), "blah", "world!")
begin
#> [1] "hello"
end
#> [1] "world!"
You can mix skips and collectors together to selectively keep and discard elements.
You can specify a default value for a left-hand side variable using
=
, similar to specifying the default value of a function
argument. This comes in handy when the number of elements returned by a
function cannot be guaranteed. tail()
for example may
return fewer elements than asked for.
However, if we tried to get the last 3 elements an error would be
raised because tail(nums, 3)
still returns only 2
values.
c(x, y, z) %<-% tail(nums, 3)
#> Error in c(x, y, z) %<-% tail(nums, 3): missing value for variable `z`
We can fix the problem by specifying a default value for
z
.