Date: 2025-05-22

# Unpacking Assignment

#### 2025-05-22

## Getting Started

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.

    c(lat, lng) %<-% list(38.061944, -122.643889)

The result is that the list is unpacked into its elements, and the elements are assigned to `lat` and `lng`.

    lat
    #> [1] 38.06194
    lng
    #> [1] -122.6439

You can also unpack the elements of a vector.

    c(lat, lng) %<-% c(38.061944, -122.643889)
    lat
    #> [1] 38.06194
    lng
    #> [1] -122.6439

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`

    c(stg1, stg2, stg3) %<-% list("Moe", "Larry", "Curley", "Donald")

### Unpacking a returned value

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.

    #
    # Convert cartesian coordinates to polar
    #
    to_polar = function(x, y) {
      c(sqrt(x^2 + y^2), atan(y / x))
    }

    c(radius, angle) %<-% to_polar(12, 5)
    radius
    #> [1] 13
    angle
    #> [1] 0.3947911

### Example: Intercept and slope of regression

You can directly unpack the coefficients of a simple linear regression into the intercept and slope.

    c(inter, slope) %<-% coef(lm(mpg ~ cyl, data = mtcars))
    inter
    #> [1] 37.88458
    slope
    #> [1] -2.87579

### Example: Unpacking the result of `safely`

The purrr package includes the `safely` function. It wraps a given function to create a new, “safe” version of the original function.

    safe_log <- purrr::safely(log)

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(10)
    pair$result
    #> [1] 2.302585
    pair$error
    #> NULL

    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 `%<-%`.

    c(res, err) %<-% safe_log(10)
    res
    #> [1] 2.302585
    err
    #> NULL

## Unpacking a data frame

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

### Example: List of data frames

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.

## Custom classes

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.

## Trailing values: the “everything else” clause

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.

## Leading values and middle values

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.

    c(begin, ..middle, end) %<-% list(1, 2, 3, 4, 5)
    begin
    #> [1] 1
    middle
    #> [[1]]
    #> [1] 2
    #>
    #> [[2]]
    #> [1] 3
    #>
    #> [[3]]
    #> [1] 4
    end
    #> [1] 5

## Skipped values: anonymous elements

You can skip one or more values using a period (`.`) instead of a variable name.

    c(x, ., z) %<-% list(1, 2, 3)
    x
    #> [1] 1
    z
    #> [1] 3

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.

    c(begin, ., ..middle, end) %<-% list(1, 2, 3, 4, 5)
    begin
    #> [1] 1
    middle
    #> [[1]]
    #> [1] 3
    #>
    #> [[2]]
    #> [1] 4
    end
    #> [1] 5

## Default values: handle missing values

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.

    nums <- c(1, 2)
    c(x, y) %<-% tail(nums, 2)
    x
    #> [1] 1
    y
    #> [1] 2

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`.

    c(x, y, z = NULL) %<-% tail(nums, 3)
    x
    #> [1] 1
    y
    #> [1] 2
    z
    #> NULL

## Swapping values

A handy trick is swapping values without the use of a temporary variable.

    c(first, last) %<-% c("Ai", "Genly")
    first
    #> [1] "Ai"
    last
    #> [1] "Genly"

    c(first, last) %<-% c(last, first)
    first
    #> [1] "Genly"
    last
    #> [1] "Ai"

or

    cat <- "meow"
    dog <- "bark"

    c(cat, dog, fish) %<-% c(dog, cat, dog)
    cat
    #> [1] "bark"
    dog
    #> [1] "meow"
    fish
    #> [1] "bark"