This package serves as an example on how to extend {constructive}. For
some users it will be enough to call constructive::.cstr_new_class()
and constructive::.cstr_new_constructor()
with commented = TRUE
and
figure it out from there. This is a more detailed walkthrough. Follow
hyperlinks to inspect relevant commits.
Let’s add support for a new class : “qr”. This is the class of the
object we get after applying base::qr()
to get the QR decomposition of
a matrix.
The reconstruction of qr objects is not perfect due to rounding errors so the feature is not a good fit for {constructive} but is a good use case for an example.
First note that {constructive} works even if it doesn’t support directly a class, it will then use the next relevant constructor:
library(constructive)
# will work but use the list constructor
A <- matrix(1:6, nrow = 3)
qr_A <- qr(A)
construct(qr_A)
#> list(
#> qr = matrix(
#> c(
#> -0x1.deeea11683f49p+1, 0.5345224838248488, 0.8017837257372732,
#> -8.55235974119758, 1.9639610121239324, 0x1.fa35f928a0dfap-1
#> ),
#> nrow = 3L,
#> ncol = 2L
#> ),
#> rank = 2L,
#> qraux = c(1.26726124191242429, 1.1499536117281517),
#> pivot = 1:2
#> ) |>
#> structure(class = "qr")
We used the following workflow:
- Call
usethis::use_package("constructive", "Suggests")
to add a soft dependency on the ‘constructive’ package if not already done. - Call
constructive::.cstr_new_class(class = "qr", constructor = "qr", commented = TRUE)
and save the template script in your “R” folder. - Call
devtools::document()
to register the methods and export and documentopt_qr()
- restart,
devtools::load_all()
, defineqr_A
By this time we can already use our new constructor, it doesn’t quite work yet though:
constructive::construct(qr_A)
#> ! The code built by {constructive} could not be evaluated.
#> ! Due to error: argument "x" is missing, with no default
#> qr()
This is because we haven’t construct the argument to qr()
!
Let’s fix this:
- Construct the argument to the
constructor.
The
qr.X()
function does the inverse transformation so the input we want to construct isqr.X(x)
. - restart,
devtools::load_all()
, defineqr_A
After this we have:
library(constructive)
construct(qr_A)
#> {constructive} couldn't create code that reproduces perfectly the input
#> ℹ Call `construct_issues()` to inspect the last issues
#>
#> qr(
#> matrix(
#> c(0.99999999999999967, 2, 3, 4.000000000000002, 5.000000000000001, 6.000000000000001),
#> nrow = 3L,
#> ncol = 2L
#> )
#> )
The recreation was not perfect due to rounding errors but we’re pretty close
construct_issues()
#> NULL
We still get the previous behavior if we use the "next"
constructor.
# fall back on the next method, which is for lists
construct(qr_A, opts_qr("next"))
#> list(
#> qr = matrix(
#> c(
#> -0x1.deeea11683f49p+1, 0.5345224838248488, 0.8017837257372732,
#> -8.55235974119758, 1.9639610121239324, 0x1.fa35f928a0dfap-1
#> ),
#> nrow = 3L,
#> ncol = 2L
#> ),
#> rank = 2L,
#> qraux = c(1.26726124191242429, 1.1499536117281517),
#> pivot = 1:2
#> ) |>
#> structure(class = "qr")
We still fail on corrupted objects though:
corrupted <- structure(1:3, class = "qr")
construct(corrupted)
#> Error in `construct()`:
#> ! {constructive} could not build the requested code.
#> Caused by error in `qr.X()`:
#> ! argument is not a QR decomposition
#> Run `rlang::last_trace()` to see where the error occurred.
Let’s fix this:
- Implement
is_corrupted_qr()
devtools::load_all()
Now we won’t fail on corrupted objects but fall back to the next method.
corrupted <- structure(1:3, class = "qr")
construct(corrupted)
#> 1:3 |>
#> structure(class = "qr")
We build a new constructor for the class “tbl_df” (tibbles), using the
deprecated constructor tibble::data_frame()
, equivalent to
tibble::tibble()
.
We used the following workflow:
- Call
usethis::use_package("constructive", "Suggests")
to add a soft dependency on the ‘constructive’ package if not already done (we did it already for the qr class) - Call
constructive::.cstr_new_constructor(class = c("tbl_df", "tbl", "data.frame"), constructor = "tibble::data_frame", commented = TRUE)
and save the template script in your “R” folder - Call
devtools::document()
to register the method - Call
devtools::load_all()
By this time we can already use our new constructor, it doesn’t quite work yet though:
construct(dplyr::band_members, opts_tbl_df("data_frame"))
#> {constructive} couldn't create code that reproduces perfectly the input
#> ℹ Call `construct_issues()` to inspect the last issues
#>
#> tibble::data_frame() |>
#> structure(row.names = c(NA, -3L))
We see that we don’t construct yet the arguments to data_frame and we
repair the row.names
attribute that the constructor should take care
of. Let’s fix this:
- Construct arguments to the
constructor.
The arguments are are the columns of the data frame, so here it’s as
simple as
args <- as.list.data.frame(x)
. We preferedas.list.data.frame()
toas.list()
because we don’t control the S3 dispatch and some corner cases might break the package. - Ignore attributes built by the constructor
- Call
devtools::load_all()
After this we have:
construct(dplyr::band_members, opts_tbl_df("data_frame"))
#> tibble::data_frame(name = c("Mick", "John", "Paul"), band = c("Stones", "Beatles", "Beatles"))
our implementation of the “tibble” constructor in
{constructive}
is very similar but handles some corner cases that don’t apply to
data_frame()
.