The original motivation for the development of this package actually was that the author found himself writing countless checks and helper code over and over again when managing parameter lists. It became apparent that something similar to python's dictionary would make life easier and so the idea of the container package was born.
The package has undergone some changes since it's initial version, but the dict as a use-case for parameter lists remains very valid. So without further ado, let's see how this works out in practice.
library(container, warn.conflicts = FALSE)
# Define some parameters
params = dict(a = 1:10, b = "foo")
With a dict the problem of accidentally overriding an existing parameter value is solved out of the box using the add
function.
params = add(params, a = 0)
# Error: name 'a' exists already
add(params, x = 0) # ok
# {a = (1L 2L 3L 4L ...), b = "foo", x = 0}
Of course, it's also possible to indeed override a parameter.
replace_at(params, a = 0)
# {a = 0, b = "foo"}
What if you intend to replace something, but there is nothing to replace?
replace_at(params, x = 0)
# Error: names(s) not found: 'x'
Now you might wonder, what if 'I don't care if it is replaced or added'. That's easy.
replace_at(params, a = 0, .add=TRUE)
# {a = 0, b = "foo"}
replace_at(params, x = 0, .add=TRUE)
# {a = (1L 2L 3L 4L ...), b = "foo", x = 0}
That is, using .add = TRUE
basically means, 'replace it, or, if it is not there, just add it'
Maybe you agree that even these simple use-cases already require some effort when using base R lists.
When extracting a parameter, you might want to be sure it exists and signal an error otherwise.
at(params, "x")
# Error: index 'x' not found
at(params, "a", "b")
# {a = (1L 2L 3L 4L ...), b = "foo"}
To extract a single raw element, use at2
at2(params, "a")
# [1] 1 2 3 4 5 6 7 8 9 10
Alternatively, you could use the standard access operators, which behave like base R list and therefore return an empty dict (or NULL
) if the index is not found.
params["x"]
# {}
params[["x"]]
# NULL
params["a"]
# {a = (1L 2L 3L 4L ...)}
params[["a"]]
# [1] 1 2 3 4 5 6 7 8 9 10
A nice property of the dict is that it provides an easy and flexible way to manage default values.
peek_at(params, "x")
# {}
peek_at(params, "x", .default = 3:1)
# {x = (3L 2L 1L)}
That is, if you peek at a non-existing parameter, by default an empty dict is returned, but with the option to explicitly set the default. This also works for multiple peeks.
peek_at(params, "a", "x", "y", .default = 3:1)
# {a = (1L 2L 3L 4L ...), x = (3L 2L 1L), y = (3L 2L 1L)}
Similar to the above examples, the user can control how removal of existing/non-existing parameters is handled. If you expect a parameter and want to be signaled if it was not there, use delete
.
delete_at(params, "x")
# Error: names(s) not found: 'x'
delete_at(params, "a") # ok
# {b = "foo"}
Otherwise to loosely delete a parameter, regardless of whether it exists or not, use discard
.
discard_at(params, "a", "x")
# {b = "foo"}
It's important to note, that the "base R list way" to delete elements does not work, because it just inserts a NULL
.
params[["a"]] <- NULL
params
# {a = NULL, b = "foo"}
Last but not least, dict allows to easily merge and/or update parameter lists.
par1 = dict(a = 1, b = "foo")
par2 = dict(b = "bar", x = 2, y = 3)
update(par1, par2)
# {a = 1, b = "bar", x = 2, y = 3}
As can be seen, existing parameters are updated and new parameters added. Using as.dict
you can also do this with ordinary lists.
update(par1, as.dict(list(b = "my b", x = 100)))
# {a = 1, b = "my b", x = 100}
That's it. I hope, it will free you some time and safe some bugs next time you need to manage parameter lists.
As a very last note, keep in mind that since container version 1.0.0, dict elements are always sorted by their name, while you are still able to access elements by position (based on the sorted values).
d = dict(x = 1, z = 2, a = 3)
d
# {a = 3, x = 1, z = 2}
d[[1]]
# [1] 3
d[2:3]
# {x = 1, z = 2}