Container operations for robust code

Container operations

Since R has always been used as an interactive tool, base R list operations are generous with respect to errors, which can lead to undetected bugs when developing code. In contrast, the container package provides additional functions for all basic operations that allow for fine control to avoid many pitfalls that can happen with lists and will usually result in more robust code.

Add elements

New elements can be added “as usual” by concatenation or name.

co <- container()
co[["x"]] <- 1
co <- c(co, 2)
co
# [x = 1, 2]

In addition, the container package provides add, which allows to add new elements by name even if that name exists already.

co = add(co, x = 3)  # same as c(co, container(x = 3))
co
# [x = 1, 2, x = 3]

Replace elements

Basic replacement again can be done “as usual” by name or position.

co[["x"]] <- 0
co[[2]] <- 12
co
# [x = 0, 12, x = 3]

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

co[[4]] <- 4
# Error: index out of range (length = 3): 4

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

co[["y"]] <- 4
co
# [x = 0, 12, x = 3, y = 4]

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

replace_at(co, x = 10, y = 13)            # name = value pairs
# [x = 10, 12, x = 3, y = 13]
replace_at(co, c("x", "y"),  c(10, 13))   # names followed by values
# [x = 10, 12, x = 3, y = 13]
replace_at(co, c(1, 4),      c(10, 13))   # positions followed by values
# [x = 10, 12, x = 3, y = 13]
replace_at(co, list(1, "y"), c(10, 13))   # mixed names/positions followed by values
# [x = 10, 12, x = 3, y = 13]

Now see how invalid indices are signaled.

replace_at(co, z = 10)
# Error: names(s) not found: 'z'
replace_at(co, "z", 10)
# Error: names(s) not found: 'z'
replace_at(co, 5, 10)
# Error: index out of range (length = 4): 5

If you instead want elements at new names to be added, set .add = TRUE. Invalid positional indices are still signaled.

co = replace_at(co, z = 10, .add = TRUE)
co = replace_at(co, 7, 10, .add = TRUE)
# Error: index out of range (length = 5): 7
co
# [x = 0, 12, x = 3, y = 4, z = 10]

It is also possible to replace elements by value, that is, you specify the value (not the index) that should be replaced. Let’s replace the 12 by “foo” and then 4 by 1:4.

co = replace(co, 12, "foo")
co
# [x = 0, "foo", x = 3, y = 4, z = 10]
co = replace(co, 4, 1:4)
co
# [x = 0, "foo", x = 3, y = (1L 2L 3L 4L), z = 10]

In an interactive R session you may want to apply the notation using curly braces.

co[[{1:4}]] <- 1:2
co
# [x = 0, "foo", x = 3, y = (1L 2L), z = 10]

Extract elements

First of all, standard extract operators apply as expected.

co[[1]]
# [1] 0
co[["x"]]
# [1] 0
co[3:5]
# [x = 3, y = (1L 2L), z = 10]
co[c("x", "y", "z")]
# [x = 0, y = (1L 2L), z = 10]

Programmatically, the corresponding functions to select one or multiple elements are named at2 and at.

at2(co, 1)
# [1] 0
at2(co, "x")
# [1] 0
at(co, 3:5)
# [x = 3, y = (1L 2L), z = 10]
at(co, c("x", "y", "z"))
# [x = 0, y = (1L 2L), z = 10]

As before you can specify mixed indices via lists.

indices = list("x", 4, "z")
at(co, indices)
# [x = 0, y = (1L 2L), z = 10]

Again, accessing non-existent names or positions is signaled with an error.

at2(co, 10)
# Error: index 10 exceeds length of Container, which is 5
at2(co, "a")
# Error: index 'a' not found
at(co, 3:6)
# Error: index 6 exceeds length of Container, which is 5
at(co, c("x", "a"))
# Error: index 'a' not found

With base R lists non-existent indices usually yield NULL.

l = list()
l[["a"]]
# NULL
l[2:3]
# [[1]]
# NULL
# 
# [[2]]
# NULL

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

peek_at2(co, "a")
# NULL
peek_at(co, 10, 11)
# []
peek_at(co, 5:10)
# [z = 10]

As you see, one important difference is multiple access via peek_at will not insert NULL values by default. However, both functions in fact allow to specify the default value that is returned if the index does not exist.

peek_at2(co, "a", default = -1)
# [1] -1

peek_at(co, "z", "a", .default = -1)
# [z = 10, a = -1]
peek_at(co, 4:8, .default = NA)
# [y = (1L 2L), z = 10, NA, NA, NA]

Remove elements

To remove elements in lists, they have to be replaced by NULL.

l = list(a = 1)
l
# $a
# [1] 1
l[["a"]] <- NULL
l
# named list()

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

co[["x"]] <- NULL
co
# [x = NULL, "foo", x = 3, y = (1L 2L), z = 10]

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

delete_at(co, 1, "y", "z")
# ["foo", x = 3]
delete(co, NULL, 1:2, 10)   # same but remove by value
# ["foo", x = 3]

As before, invalid indices or missing values are signaled.

delete_at(co, "a")
# Error: names(s) not found: 'a'
delete_at(co, 10)
# Error: index out of range (length = 5): 10

delete(co, 1:3)
# Error: (1L 2L 3L) is not in Container

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

discard_at(co, 1, "a")
# ["foo", x = 3, y = (1L 2L), z = 10]
discard_at(co, 1:100)
# []

discard(co, NULL, 1:2, 1:3, 1:4)
# ["foo", x = 3, z = 10]

Combine containers

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

c1 = container(1, b = 2)
c2 = container(   b = 0, c = 3)

update(c1, c2)
# [1, b = 0, c = 3]
update(c2, c1)
# [b = 2, c = 3, 1]

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

l1 = list(1, b = 2)
l2 = list(   b = 0, c = 3)

update(l1, l2)
# [[1]]
# [1] 1
# 
# $b
# [1] 0
# 
# $c
# [1] 3
update(l2, l1)
# $b
# [1] 2
# 
# $c
# [1] 3
# 
# [[3]]
# [1] 1