---
title: "Use container for code development"
output:
  rmarkdown::html_vignette:
    toc: true
    toc_depth: 4
description: >
    Use this vignette if you want to see how container operations can be used
    to make it safer and easier to work with list-like data structures.
    It describes the different methods to add, extract, replace, and remove
    elements and their nuances.
    This vignette demonstrates how {container} enhances the robustness of
    code by providing strict validation and clear, intent-driven operations
    for adding, extracting, replacing, and removing elements.
    It highlights features that minimize errors and streamline development
    workflows.
vignette: >
  %\VignetteIndexEntry{Use container for code development}
  %\VignetteEngine{knitr::rmarkdown}
  %\VignetteEncoding{UTF-8}
---

```{r knitr-setup, include = FALSE}
require(container)
knitr::opts_chunk$set(
  comment = "#",
    error = FALSE,
     tidy = FALSE,
    cache = FALSE,
 collapse = TRUE
)
old <- options(width = 100L)
```

## Robust usage

Base R's list operations are designed for interactive use, offering
flexibility but often being overly permissive with user input.
While convenient, this can lead to subtle bugs during development
and requires additional, sometimes tedious, checks to ensure code
robustness.

The {container} package addresses these challenges by providing
operations that explicitly define the intent of each action. By
prioritizing clarity and precision, {container} enables you
to write leaner and more reliable code right from the start.

This vignette revisits some of the basic operations familiar from
base R lists and demonstrates how {container} enhances them with
strict validation and powerful additional features.

```{r}
co <- container()
```

### Add

Using base R *lists* notation, elements are usually added by name or
concatenation.

```{r}
co[["x"]] <- 1
co <- c(co, 2)

co
```

The {container} package provides the `add` function to add elements.

```{r}
co <- add(co, x = 3)  # same as c(co, x = 3)

co
```

For container objects there is not much of a difference between the two
methods. Now, if for example you don't want to allow duplicated names,
you can use dict objects instead. These are a subclass of container and
would throw an error in this case.

```{r, error = TRUE}
d <- dict(x = 1)

add(d, x = 3)
```

For more details see the reference documentation or have a look at
the [Deque, Set, and Dict](articles/v05-deque-set-dict.html) vignette.
Lastly, note that the base append function also works with containers.

```{r}
append(co, 1.5, after = 1)
```



### Replace

As demonstrated before, elements can be loosely replaced by index or name.

```{r}
co[["x"]] <- 0
co[[2]] <- 12

co
```

Also, in contrast to base *lists*, the container will not allow to
add elements at positions longer than the length of the object.

```{r, error = TRUE}
co[[4]] <- 3
```

If the name does not exist, the element is appended as known from base *lists*.

```{r}
co[["y"]] <- 5

co
```

#### Strict replace

Let's imagine you want to replace an element of a certain name, and therefore
expect that the name exists already. In code development, this would require
an additional check, for example:

```{r, eval = FALSE}
name <- "z"
if (name %in% names(co)) {
  co[[name]] <- 10
} else {
  stop("Name '", name,  "' does not exist.")
}
```

Clearly this is a lot of boilerplate code for a simple operation, and it
is easy to forget such checks. In addition, you end up with a lot of
unit tests basically to check the checks. Last but not
least, the intent of the code is not as clear as it could be.

This is where the {container} package comes in. If you want to make sure
that something is replaced, {container} provides the
function `replace_at`, which will only replace elements at names or
positions that exist. The following statements are all equal and show the
different possibilities on how to use `replace_at`.

```{r}
replace_at(co, x = 10, y = 13)            # name = value pairs

replace_at(co, c("x", "y"),  c(10, 13))   # names followed by values

replace_at(co, c(1, 4),      c(10, 13))   # positions followed by values

replace_at(co, list(1, "y"), c(10, 13))   # mixed indices followed by values
```

Next, let's see how invalid indices are signaled.

```{r, error = TRUE}
replace_at(co, z = 10)

replace_at(co, "z", 10)

replace_at(co, 5, 10)
```

If you instead don't mind that elements at new names will be added,
set `.add = TRUE`. Invalid positional indices are still signaled.

```{r, error = TRUE}
co <- replace_at(co, z = 10, .add = TRUE)   # ok

co <- replace_at(co, 7, 10, .add = TRUE)

co
```


#### Strict replace by value

It is also possible to replace elements by value, that is, you specify the
value (not the index) that should be replaced.
To see this, let's replace `12` (located at the 2nd postion) by `"foo"` and
then `y = 5` (located at the 4th position) by `1:2`.

```{r}
co <- replace(co, old = 12, new = "foo")
co

co <- replace(co, old = 5, new = 1:2)
co
```

Implementing this "manually" would require even more additional code as before.
As intended, if the value does not exist, an error is signaled.

```{r, error = TRUE}
replace(co, old = "non-existent-value", new = "my value")
```

Again, the intend that you want to replace but don't mind that the
element is added can be specified:

```{r}
replace(co, old = "non-existent-value", new = "my value", add = TRUE)
```

### Extract

Let's recap the standard extract operators.

```{r}
co[[1]]

co[["x"]]

co[3:5]

co[c("x", "y", "z")]
```

#### Strict extract

The {container} functions to strictly select one or multiple elements are
named `at2` and `at`.^[Resembling R base-internal .subset2 and .subset.]

```{r}
at2(co, 1)

at2(co, "x")

at(co, 3:5)

at(co, c("x", "y", "z"))
```

As before you can specify mixed indices via lists.

```{r}
indices <- list("x", 4, "z")

at(co, indices)
```

Accessing non-existent names or positions is signaled with an error as follows.

```{r, error = TRUE}
at2(co, 10)

at2(co, "a")

at(co, 3:6)

at(co, c("x", "a"))
```

Be reminded that with base *lists* non-existent indices just would have
returned `NULL` values.

```{r}
l <- list()

l[2:3]

l[["a"]]
```

If needed, the (less strict) *list* access can be mimicked with `peek_at` and
`peek_at2`.

```{r}
co

peek_at(co, 10, 11)

peek_at(co, 5:10)

peek_at2(co, "a")
```

As you see, one important difference is that multiple access via `peek_at`
by default instead of `NULL` values just returns nothing.
However, both functions allow to specify a custom default value being
returned if the index does not exist.

```{r}
co

peek_at2(co, "a", default = -1)

peek_at(co, "z", "a", .default = -1)

peek_at(co, 4:8, .default = NA)
```


### Remove

To remove elements in lists, they are usually replaced by `NULL`.

```{r}
l <- list(a = 1)

l

l[["a"]] <- NULL

l
```

With the container package this is done differently, as replacing by `NULL`
will not delete the element but literally replace it by `NULL`.

```{r}
co[["x"]] <- NULL

co
```

Instead, elements can be deleted by index (`delete_at`) or value (`delete`)
as follows.

```{r}
co

delete_at(co, 1, "y", "z")

delete(co, NULL, 1:2, 10)   # same but remove by value
```

As before, invalid indices or missing values are signaled.

```{r, error = TRUE}
co

delete_at(co, "a")

delete_at(co, 10)

delete(co, 1:3)
```


If you need a less strict delete operation, use the `discard` functions, which
delete all valid indices or values and ignore the rest.

```{r}
co

discard_at(co, 1, "a")

discard_at(co, 1:100)

discard(co, NULL, 1:2, 1:3, 1:4)    # discard by value
```


## Combine containers

The `update` function is used to combine/merge two containers.

```{r}
c1 <- container(1, b = 2)
c2 <- container(   b = 0, c = 3)

update(c1, c2)

update(c2, c1)
```

With the container package this function is also provided for base R *lists*.

```{r}
l1 <- list(1, b = 2)
l2 <- list(   b = 0, c = 3)

update(l1, l2)

update(l2, l1)
```

Note that there is a similar function `utils::modifyList`, which, however,
in contrast to `update`, does *not* "forward" unnamed elements.

```{r}
modifyList(l1, l2)

modifyList(l2, l1)  # drops l1[[1]] = 1
```

Also, while `utils::modifyList` modifies a list recursively by changing
a subset of elements at each level, `update` just works on the first level.

```{r}
l1 <- list(a = 1, b = list(c = "a", d = FALSE))

l2 <- list(e = 2, b = list(d = TRUE))

modifyList(l1, l2)  # modifies l1$b$d from FALSE to TRUE

update(l1, l2)      # replaces l1$b by l2$b
```


## Functional programming

The apply family and common higher-order functions both can be used with
containers as usual.

```{r}
co <- container(a = 1, b = 2, c = 3, d = 4)

sapply(co, function(x) x^2)

Filter(co, f = function(x) x > 2)

Reduce(co, f = sum)
```

## Summary

This vignette demonstrates how {container} enhances robust code development by providing:

* Clear and intent-driven operations for adding, replacing, extracting, and removing
  elements, minimizing boilerplate and potential errors.
* Strict validation methods like replace_at() and at() for safer and more precise
  modifications and access.
* Flexible tools like peek_at() and discard() for handling invalid or non-existent
  indices gracefully.
* Safe merging of containers and lists with update()
* Full compatibility with functional programming tools like sapply(), Filter(), and
  Reduce() for streamlined workflows.


To see how some of the functions disussed here are applied with derived
data structures, see:

* [Manage parameter lists with dict](v03-parameter-list.html)
* [Manage data columns with dict.table](v04-manage-data-columns.html)

```{r, include = FALSE}
options(old)
```
