```{r, child = "../setup.Rmd", include = FALSE} ``` ```{r, include = FALSE} old_warn_on_fallback <- options(`vctrs:::warn_on_fallback` = FALSE) knitr_defer(options(old_warn_on_fallback)) ``` This guide illustrates how to implement `vec_ptype2()` and `vec_cast()` methods for existing classes. Related topics: - For an overview of how these generics work and their roles in vctrs, see [`?theory-faq-coercion`][theory-faq-coercion]. - For an example of implementing coercion methods for data frame subclasses, see [`?howto-faq-coercion-data-frame`][howto-faq-coercion-data-frame]. - For a tutorial about implementing vctrs classes from scratch, see `vignette("s3-vector")` ## The natural number class We'll illustrate how to implement coercion methods with a simple class that represents natural numbers. In this scenario we have an existing class that already features a constructor and methods for `print()` and subset. ```{r} #' @export new_natural <- function(x) { if (is.numeric(x) || is.logical(x)) { stopifnot(is_whole(x)) x <- as.integer(x) } else { stop("Can't construct natural from unknown type.") } structure(x, class = "my_natural") } is_whole <- function(x) { all(x %% 1 == 0 | is.na(x)) } #' @export print.my_natural <- function(x, ...) { cat("\n") x <- unclass(x) NextMethod() } #' @export `[.my_natural` <- function(x, i, ...) { new_natural(NextMethod()) } ``` ```{r, include = FALSE} # Necessary because includeRmd() evaluated in a child of global knitr_local_registration("base::print", "my_natural") knitr_local_registration("base::[", "my_natural") ``` ```{r} new_natural(1:3) new_natural(c(1, NA)) ``` ## Roxygen workflow ```{r, child = "snippet-roxy-workflow.Rmd"} ``` ## Implementing `vec_ptype2()` ### The self-self method The first method to implement is the one that signals that your class is compatible with itself: ```{r} #' @export vec_ptype2.my_natural.my_natural <- function(x, y, ...) { x } vec_ptype2(new_natural(1), new_natural(2:3)) ``` ```{r, include = FALSE} # Necessary because includeRmd() evaluated in a child of global knitr_local_registration("vctrs::vec_ptype2", "my_natural.my_natural") ``` `vec_ptype2()` implements a fallback to try and be compatible with simple classes, so it may seem that you don't need to implement the self-self coercion method. However, you must implement it explicitly because this is how vctrs knows that a class that is implementing vctrs methods (for instance this disable fallbacks to `base::c()`). Also, it makes your class a bit more efficient. ### The parent and children methods Our natural number class is conceptually a parent of `` and a child of ``, but the class is not compatible with logical, integer, or double vectors yet: ```{r, error = TRUE} vec_ptype2(TRUE, new_natural(2:3)) vec_ptype2(new_natural(1), 2:3) ``` We'll specify the twin methods for each of these classes, returning the richer class in each case. ```{r} #' @export vec_ptype2.my_natural.logical <- function(x, y, ...) { # The order of the classes in the method name follows the order of # the arguments in the function signature, so `x` is the natural # number and `y` is the logical x } #' @export vec_ptype2.logical.my_natural <- function(x, y, ...) { # In this case `y` is the richer natural number y } ``` Between a natural number and an integer, the latter is the richer class: ```{r} #' @export vec_ptype2.my_natural.integer <- function(x, y, ...) { y } #' @export vec_ptype2.integer.my_natural <- function(x, y, ...) { x } ``` ```{r, include = FALSE} # Necessary because includeRmd() evaluated in a child of global knitr_local_registration("vctrs::vec_ptype2", "my_natural.logical") knitr_local_registration("vctrs::vec_ptype2", "my_natural.integer") knitr_local_registration("vctrs::vec_ptype2", "integer.my_natural") knitr_local_registration("vctrs::vec_ptype2", "logical.my_natural") ``` We no longer get common type errors for logical and integer: ```{r} vec_ptype2(TRUE, new_natural(2:3)) vec_ptype2(new_natural(1), 2:3) ``` We are not done yet. Pairwise coercion methods must be implemented for all the connected nodes in the coercion hierarchy, which include double vectors further up. The coercion methods for grand-parent types must be implemented separately: ```{r} #' @export vec_ptype2.my_natural.double <- function(x, y, ...) { y } #' @export vec_ptype2.double.my_natural <- function(x, y, ...) { x } ``` ```{r, include = FALSE} # Necessary because includeRmd() evaluated in a child of global knitr_local_registration("vctrs::vec_ptype2", "my_natural.double") knitr_local_registration("vctrs::vec_ptype2", "double.my_natural") ``` ### Incompatible attributes Most of the time, inputs are incompatible because they have different classes for which no `vec_ptype2()` method is implemented. More rarely, inputs could be incompatible because of their attributes. In that case incompatibility is signalled by calling `stop_incompatible_type()`. In the following example, we implement a self-self ptype2 method for a hypothetical subclass of `` that has stricter combination semantics. The method throws when the levels of the two factors are not compatible. ```{r, eval = FALSE} #' @export vec_ptype2.my_strict_factor.my_strict_factor <- function(x, y, ..., x_arg = "", y_arg = "") { if (!setequal(levels(x), levels(y))) { stop_incompatible_type(x, y, x_arg = x_arg, y_arg = y_arg) } x } ``` Note how the methods need to take `x_arg` and `y_arg` parameters and pass them on to `stop_incompatible_type()`. These argument tags help create more informative error messages when the common type determination is for a column of a data frame. They are part of the generic signature but can usually be left out if not used. ## Implementing `vec_cast()` Corresponding `vec_cast()` methods must be implemented for all `vec_ptype2()` methods. The general pattern is to convert the argument `x` to the type of `to`. The methods should validate the values in `x` and make sure they conform to the values of `to`. Please note that for historical reasons, the order of the classes in the method name is in reverse order of the arguments in the function signature. The first class represents `to`, whereas the second class represents `x`. The self-self method is easy in this case, it just returns the target input: ```{r} #' @export vec_cast.my_natural.my_natural <- function(x, to, ...) { x } ``` The other types need to be validated. We perform input validation in the `new_natural()` constructor, so that's a good fit for our `vec_cast()` implementations. ```{r} #' @export vec_cast.my_natural.logical <- function(x, to, ...) { # The order of the classes in the method name is in reverse order # of the arguments in the function signature, so `to` is the natural # number and `x` is the logical new_natural(x) } vec_cast.my_natural.integer <- function(x, to, ...) { new_natural(x) } vec_cast.my_natural.double <- function(x, to, ...) { new_natural(x) } ``` ```{r, include = FALSE} # Necessary because includeRmd() evaluated in a child of global knitr_local_registration("vctrs::vec_cast", "my_natural.my_natural") knitr_local_registration("vctrs::vec_cast", "my_natural.logical") knitr_local_registration("vctrs::vec_cast", "my_natural.integer") knitr_local_registration("vctrs::vec_cast", "my_natural.double") ``` With these methods, vctrs is now able to combine logical and natural vectors. It properly returns the richer type of the two, a natural vector: ```{r} vec_c(TRUE, new_natural(1), FALSE) ``` Because we haven't implemented conversions _from_ natural, it still doesn't know how to combine natural with the richer integer and double types: ```{r, error = TRUE} vec_c(new_natural(1), 10L) vec_c(1.5, new_natural(1)) ``` This is quick work which completes the implementation of coercion methods for vctrs: ```{r} #' @export vec_cast.logical.my_natural <- function(x, to, ...) { # In this case `to` is the logical and `x` is the natural number attributes(x) <- NULL as.logical(x) } #' @export vec_cast.integer.my_natural <- function(x, to, ...) { attributes(x) <- NULL as.integer(x) } #' @export vec_cast.double.my_natural <- function(x, to, ...) { attributes(x) <- NULL as.double(x) } ``` ```{r, include = FALSE} # Necessary because includeRmd() evaluated in a child of global knitr_local_registration("vctrs::vec_cast", "logical.my_natural") knitr_local_registration("vctrs::vec_cast", "integer.my_natural") knitr_local_registration("vctrs::vec_cast", "double.my_natural") ``` And we now get the expected combinations. ```{r} vec_c(new_natural(1), 10L) vec_c(1.5, new_natural(1)) ```