I'm experimenting with the vctrs
package. My actual use-case is in relevant aspects similar to the rational
class implemented in the helpful S3 vectors article on the vctrs
homepage, in that it uses rcrd
for paired data. I'll use that for my reprex for clarity. (EDIT: I am not, however, specifically interested in rationals.) Let me paste the relevant parts first:
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)
}
format.vctrs_rational <- function(x, ...) {
n <- field(x, "n")
d <- field(x, "d")
out <- paste0(n, "/", d)
out[is.na(n) | is.na(d)] <- NA
out
}
vec_ptype_abbr.vctrs_rational <- function(x, ...) "rtnl"
vec_ptype_full.vctrs_rational <- function(x, ...) "rational"
An example of using this:
(x <- rational(1, 1:15))
#> <rational[15]>
#> [1] 1/1 1/2 1/3 1/4 1/5 1/6 1/7 1/8 1/9 1/10 1/11 1/12 1/13 1/14 1/15
My problem arises when trying to use a class like this in a matrix
:
matrix(x, ncol = 5, nrow = 3)
#> Warning in matrix(x, ncol = 5, nrow = 3): data length [2] is not a sub-multiple
#> or multiple of the number of rows [3]
#> [,1] [,2] [,3] [,4] [,5]
#> [1,] Integer,15 Integer,15 Integer,15 Integer,15 Integer,15
#> [2,] Integer,15 Integer,15 Integer,15 Integer,15 Integer,15
#> [3,] Integer,15 Integer,15 Integer,15 Integer,15 Integer,15
Created on 2020-06-05 by the reprex package (v0.3.0)
I was hoping to get a 3-by-5 matrix with each cell containing one value from x
, as would have happened if x
had been a "normal" vector. Instead, I get a 3-by-5 matrix of lists, where vctrs
tries to make alternating rows contain n
and d
values, respectively.
My question, therefore, is is it possible to get vctrs
to work with matrices in the "expected" manner for a situation like this, and if so, how? By experimenting, I got the sense that this might have to do with implementing dim.rational
and `dim<-.rational`
, but I couldn't make it work.
EDIT: If the desired matrix is not clear (as suggested in the comments), I would like a matrix object somewhat akin to the following, which I've edited by hand:
(m <- matrix(x, ncol = 5, nrow = 3))
#> <rational[15]>
#> [,1] [,2] [,3] [,4] [,5]
#> [1,] 1/1 1/4 1/7 1/10 1/13
#> [2,] 1/2 1/5 1/8 1/11 1/14
#> [3,] 1/3 1/6 1/9 1/12 1/15
Such that normal matrix operations would work on m
, e.g.
m[1,]
#> <rational[5]>
#> 1/1 1/4 1/7 1/10 1/13
The whole design of the rational
class seems built on preserving its type safety, and hiding implementation from users, which I can see would be necessary to get it to work consistently, but this means that you can't expect it to play nicely with R's default S3 methods.
The help file for vctrs
specifically says
- dims(), dims<-, dimnames(), dimnames<-, levels(), and levels<- methods throw errors.
This suggests that the authors of vctrs
didn't think it was a great base on which to build matrix methods.
In any case, I wouldn't be in such a hurry to try to get it into a matrix, since you can't do anything with it once it's there: there are no arithmetic methods available to you:
x + 2
#> Error: <rational> + <double> is not permitted
#> Run `rlang::last_error()` to see where the error occurred.
x * 2
#> Error: <rational> * <double> is not permitted
#> Run `rlang::last_error()` to see where the error occurred.
x + x
#> Error: <rational> + <rational> is not permitted
#> Run `rlang::last_error()` to see where the error occurred.
So you would need to define the arithmetic methods first. Before you even do that, you need $
accessors for the numerators and denominators, an is.rational
function to check the type before attempting arithmetic, a function to find the greatest common denominator, and a function to simplify your rationals based on it.
`$.vctrs_rational` <- function(vec, symb) unclass(vec)[[as.character(symb)]]
is.rational <- function(num) class(num)[1] == "vctrs_rational"
gcd <- function(x, y) ifelse(x %% y, gcd(y, x %% y), y)
simplify <- function(num) {
common <- gcd(num$n, num$d)
rational(num$n / common, num$d/common)
}
So now you can do
x$n
#> [1] 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
x$d
#> [1] 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
is.rational(x)
#> [1] TRUE
And now write the arithmetic functions. For example, here is an implementation of basic arithmetic to cover numeric and rational types:
Ops.vctrs_rational <- function(vec, num)
{
if(!is.rational(vec)) {tmp <- vec; vec <- num; num <- tmp; }
if(.Generic == '*'){
if(is.rational(num)) return(simplify(rational(vec$n * num$n, vec$d * num$d)))
else return(simplify(rational(vec$n * 2, vec$d)))
}
else if (.Generic == '/'){
if(is.rational(num)) return(vec * rational(num$d, num$n))
else return(vec * rational(1, num))
}
else if (.Generic == '+'){
if(is.rational(num)){
new_n <- vec$n * (vec$d * num$d)/vec$d + num$n * (vec$d * num$d)/num$d
return(simplify(rational(new_n, vec$d * num$d)))
}
else return(simplify(rational(num * vec$d + vec$n, vec$d)))
}
else if (.Generic == '-'){
if(is.rational(num)) return(vec + rational(-num$n, num$d))
else return(vec + (-num))
}
else if (.Generic == '^'){
if(is.rational(num) | num < 0) stop("fractional and negative powers not supported")
return(simplify(rational(vec$n ^ num, vec$d ^ num)))
}
}
This now allows you to do, for example:
x * 3
#> <rational[15]>
#> [1] 3/1 3/2 1/1 3/4 3/5 1/2 3/7 3/8 1/3 3/10 3/11 1/4 3/13 3/14 1/5
x + x
#> <rational[15]>
#> [1] 2/1 1/1 2/3 1/2 2/5 1/3 2/7 1/4 2/9 1/5 2/11 1/6 2/13 1/7 2/15
(2 + x)^2 / (3 * x + 1)
#> <rational[15]>
#> [1] 3/1 25/8 49/15 27/8 121/35 169/48 25/7 289/80
#> [9] 361/99 147/40 529/143 625/168 243/65 841/224 961/255
Trying to use matrix()
itself directly is probably not going to work, since matrix
works by converting to a base vector and then calling C code. This strips out class information.
That means you need to define a separate rational_matrix
class, which in turn would benefit from a supporting rational_vector
class. We can then define specific format and print methods:
as.vector.vctrs_rational <- function(x, ...) {
n <- x$n/x$d
attr(n, "denom") <- x$d
attr(n, "numerator") <- x$n
class(n) <- "rational_attr"
n
}
rational_matrix <- function(data, nrow = 1, ncol = 1,
byrow = FALSE, dimnames = NULL){
d <- as.vector(data)
m <- .Internal(matrix(d, nrow, ncol, byrow, dimnames, missing(nrow),
missing(ncol)))
m_dim <- dim(m)
attributes(m) <- attributes(d)
dim(m) <- rev(m_dim)
class(m) <- c("rational_matrix", "matrix")
m
}
format.rational_matrix <- function(x) {
return(paste0(attr(x, "numerator"), "/", attr(x, "denom")))
}
print.rational_matrix <- function(x)
{
print(matrix(format(x), nrow = dim(x)[2]), quote = FALSE)
}
Finally, you need to overwrite matrix()
to make it an S3 method, being sure you first copy the function as matrix.default
matrix.default <- matrix
matrix <- function(data = NA, ...) UseMethod("matrix")
matrix.vctrs_rational <- function(data, ...) rational_matrix(data, ...)
So now you can do:
matrix(x, nrow = 5)
#> [,1] [,2] [,3] [,4] [,5]
#> [1,] 1/1 1/4 1/7 1/10 1/13
#> [2,] 1/2 1/5 1/8 1/11 1/14
#> [3,] 1/3 1/6 1/9 1/12 1/15
rational_matrix(x + 5, nrow = 3)
#> [,1] [,2] [,3] [,4] [,5]
#> [1,] 6/1 21/4 36/7 51/10 66/13
#> [2,] 11/2 26/5 41/8 56/11 71/14
#> [3,] 16/3 31/6 46/9 61/12 76/15
rational_matrix(x + x, nrow = 5)
#> [,1] [,2] [,3]
#> [1,] 2/1 1/3 2/11
#> [2,] 1/1 2/7 1/6
#> [3,] 2/3 1/4 2/13
#> [4,] 1/2 2/9 1/7
#> [5,] 2/5 1/5 2/15
However, to get this to work we had to add extra classes with attributes anyway, so my feeling is that if you want a rational class that works with matrices etc, you should do it in native S3 or one of the other object-oriented approaches available in R rather than using the vctrs
package.
It's also worth saying that the above class is far from production-ready, since you would need to add in methods to test equality / inequality, methods to describe the matrix operations, an ability to convert to decimal, plotting methods, etc.