The vctrs S3 vectors vignette advises says that user-friendly constructors should return a zero-length vector when called with no arguments, as in the percent() example:
library("vctrs")
new_percent <- function(x = double()) {
vec_assert(x, double())
new_vctr(x, class = "vctrs_percent")
}
percent <- function(x = double()) {
x <- vec_cast(x, double())
new_percent(x)
}
percent()
#> <vctrs_percent[0]>
The explanation is that this makes it easier to use as a prototype, as in vec_cast(x, percent())
.
But later, the rational() example does not follow this advice:
library("vctrs")
library("zeallot")
new_rational <- function(n = integer(), d = integer()) {
vec_assert(n, ptype = integer())
vec_assert(d, ptype = integer())
new_rcrd(list(n = n, d = d), class = "vctrs_rational")
}
rational <- function(n, d) {
c(n, d) %<-% vec_cast_common(n, d, .to = integer())
c(n, d) %<-% vec_recycle_common(n, d)
new_rational(n, d)
}
rational()
#> Error in vec_cast_common(n, d, .to = integer()): argument "n" is missing, with no default
So presumably to invoke the rational class as a prototype, you would use vec_cast(x, new_rational())
.
I'm struggling to understand which pattern to use when implementing my own vctrs-based classes. On the one hand, it is useful that percent() returns something that works as a prototype, because to allow the user to do the same with rational you'd have to export the internal constructor new_rational(), which doesn't seem ideal. On the other hand, it is awkward to maintain this behaviour with more complex constructors, e.g. with defaults and required arguments, and ends up with a signature which is not as easy for the user to understand.
Is there any advice on if/when it's acceptable for constructors not to return a zero-length vector? Does it matter if the class is a vctr or rcrd? And if you write one that doesn't, should the low-level constructor new_x() be exported and use as a prototype?