We detail in this vignette how {constructive} works and how you might
extend it by defining custom
.cstr_construct.<class>() and
.cstr_construct.<class>.<constructor>()
methods.
The functions .cstr_new_class() and
.cstr_new_constructor() can be used to create custom
constructors from templates. The easiest workflow is probably to take a
look at the package {constructive.examples}
which will guide you through the process, then call these functions with
the argument commented = TRUE.
If this is not enough, or if you want to know more, we describe below in more details how the package and its key functions work.
construct() and .cstr_construct().cstr_construct() builds code recursively, without
checking the inputs or output validity, without error handling, and
without formatting.construct() wraps .cstr_construct() and
does this extra work..cstr_construct() is a generic and many methods are
implemented in the package, for instance construct(iris)
will call .cstr_construct.data.frame() which down the line
will call .cstr_construct.numeric() and
.cstr_construct.factor() to construct its columns..cstr_construct() contains extra logic to :
data argument.classes argument and the
functions construct_base() and
construct_dput() can work..cstr_construct
#> function (x, ..., data = NULL, classes = NULL)
#> {
#> data_name <- perfect_match(x, data)
#> if (!is.null(data_name))
#> return(data_name)
#> if (is.null(classes)) {
#> UseMethod(".cstr_construct")
#> }
#> else if (identical(classes, "-")) {
#> .cstr_construct.default(x, ..., classes = classes)
#> }
#> else if (classes[[1]] == "-") {
#> cl <- setdiff(.class2(x), classes[-1])
#> UseMethod(".cstr_construct", structure(NA_integer_, class = cl))
#> }
#> else {
#> cl <- intersect(.class2(x), classes)
#> UseMethod(".cstr_construct", structure(NA_integer_, class = cl))
#> }
#> }
#> <bytecode: 0x14299c980>
#> <environment: namespace:constructive>.cstr_construct.<class>() methods.cstr_construct.<class>() methods are generics
themselves, they typically have this form:
.cstr_construct.Date <- function(x, ...) {
opts <- list(...)$opts$Date %||% opts_Date()
if (is_corrupted_Date(x) || opts$constructor == "next") return(NextMethod())
UseMethod(".cstr_construct.Date", structure(NA, class = opts$constructor))
}opts contains options passed to the ... or
template arguments of construct() through the
opts_*() functions, we fall back to a default value if none
were provided. The list(...)$opts idiom is used in various
places, it allows us to forward the dots conveniently.NextMethod() to forward all our inputs to a lower
level constructor.UseMethod() call above.opts_<class>() functionWhen implementing a new method you’ll need to define and export the
corresponding opts_<class>() function. It provides to
the user with a way to choose a constructor and additional parameters,
and sets the default behavior.
It should always have this form:
opts_Date <- function(
constructor = c(
"as.Date", "as_date", "date", "new_date", "as.Date.numeric", "as_date.numeric", "next", "double"),
...,
origin = "1970-01-01") {
.cstr_options("Date", constructor = constructor[[1]], ..., origin = origin)
}opts_<class>() function and as the first argument of
.cstr_options().originmatch.arg() here, because new
constructors can be defined outside of the package.is_corrupted_<class>() functionis_corrupted_<class>() checks if x
has the right internal type and attributes, sometimes structure, so that
it satisfies the expectations of a well formatted object of a given
class.
If an object is corrupted for a given class we cannot use
constructors for this class, so we move on to a lower level constructor
by calling NextMethod() in
.cstr_construct().
This is important so that {constructive} doesn’t choke
on corrupted objects but instead helps us understand them.
For instance in the following example x prints like a
date but it’s corrupted, a date should not be built on top of characters
and this object cannot be built with as.Date() or other
idiomatic date constructors.
We have defined :
And as a consequence the next method,
.cstr_construct.default() will be called through
NextMethod() and will handle the object using an atomic
vector constructor:
constructors are functions named as
.cstr_construct.<class>.<constructor>.
For instance the default constructor for “Date” is :
constructive:::.cstr_construct.Date.as.Date
#> function (x, ...)
#> {
#> opts <- list(...)$opts$Date %||% opts_Date()
#> origin <- opts$origin
#> compatible_with_char <- all(rlang::is_integerish(x) & (is.finite(x) |
#> (is.na(x) & !is.nan(x))))
#> if (!compatible_with_char || all(is.na(x))) {
#> return(.cstr_construct.Date.as.Date.numeric(x, ...))
#> }
#> code <- .cstr_apply(list(format(x)), "as.Date", ..., new_line = FALSE)
#> repair_attributes_Date(x, code, ...)
#> }
#> <bytecode: 0x142c1a6b0>
#> <environment: namespace:constructive>Their arguments are x and ..., and not
more. Additional parameters fed to the opt_<class>()
function can be fetched from
list(...)$opts$<class>
The function .cstr_apply() is used to construct
arguments recursively.
Sometimes a constructor cannot handle all cases and we need to fall
back to another constructor, it happens below because Inf,
NA, or decimal dates cannot be represented by a string
wrapped by as.Date().
x <- structure(c(12345, 20000), class = "Date")
y <- structure(c(12345, Inf), class = "Date")
construct(x)
#> as.Date(c("2003-10-20", "2024-10-04"))That last line of the function is essential, it does the attribute repair.
Constructors should always end by a call to
.cstr_repair_attributes() or a function that wraps it.
These are needed to adjust the attributes of an object after
idiomatic constructors such as as.Date() have defined their
data and canonical attributes.
x <- structure(c(12345, 20000), class = "Date", some_attr = 42)
# attributes are not visible due to "Date"'s printing method
x
#> [1] "2003-10-20" "2024-10-04".cstr_repair_attributes() essentially sets attributes
with exceptions :
idiomatic_class
argumentconstructive:::repair_attributes_Date
#> function (x, code, ...)
#> {
#> .cstr_repair_attributes(x, code, ..., idiomatic_class = "Date")
#> }
#> <bytecode: 0x131325d28>
#> <environment: namespace:constructive>