3

I'm trying to understand how to build objects with vectors. I thought this was straightforwards, but then had trouble when I used c() on my object.

Our object has two attributes, x and descriptor, both strings in this case (my object will have attributes with differing types). We've built a constructor, new_toy_vector. I haven't built a convenience function in this example yet.

new_toy_vector <- function(
  x = character(),
  descriptor = character()) {

  vctrs::vec_assert(x,character())
  vctrs::vec_assert(descriptor, character())

  vctrs::new_vctr(x,
                  descriptor = descriptor,
                  class = "toy_vector")
}



format.toy_vector <- function(x, ...) {
  paste0(vctrs::vec_data(x)," is ", attr(x, "descriptor"))
}

obj_print_data.toy_vector <- function(x) {
  cat(format(x), sep = "\n")
}

c(new_toy_vector("Hello", "Foo"), new_toy_vector("World", "Bar"))
#> Error: No common type for `..1` <toy_vector> and `..2` <toy_vector>.

Created on 2020-04-26 by the reprex package (v0.3.0)

I then tried to create a coercion with itself unless the default method wasn't defined for some reason:

> vec_ptype2.toy_vector.toy_vector <- function(x, y, ...) new_toy_vector()
> c(new_toy_vector("Hello", "Foo"), new_toy_vector("World", "Bar"))
Error: Can't convert <toy_vector> to <toy_vector>.

Any ideas what I'm missing or misunderstanding? Why can't I combine the two objects in the example?

Andrew Hill
  • 307
  • 2
  • 12

4 Answers4

2

Generally attributes are not subsetted when an object is subsetted, this is not a rule and the "names" attribute is a prominent example which doesn't follow this practice. To create an attribute that behaves like "names" you'd have to jump through hoops, and {vctrs} was designed to simplify this kind of tasks for you.

The way we do this with {vctrs} is by using records, and we won't need attributes :

Record-style objects use a list of equal-length vectors to represent individual components of the object. The best example of this is POSIXlt, which underneath the hood is a list of 11 fields like year, month, and day. Record-style classes override length() and subsetting methods to conceal this implementation detail.

Using the example in the link above as a template we can implement your case :

new_toy_vector <- function(
  value = character(),
  descriptor = character()) {
  vctrs::vec_assert(value,character())
  vctrs::vec_assert(descriptor, character())
  vctrs::new_rcrd(list(value = value, descriptor = descriptor), class = "toy_vector")
}


format.toy_vector <- function(x, ...) {
  value <- vctrs::field(x, "value")
  descriptor <- vctrs::field(x, "descriptor")
  paste0('"', value," is ", descriptor, '"')
}

v1 <- new_toy_vector(
  c("Hello", "World"), 
  c("Foo", "Bar"))

v2 <- c(
  new_toy_vector("Hello", "Foo"), 
  new_toy_vector("World", "Bar"))

v1
#> <toy_vector[2]>
#> [1] "Hello is Foo" "World is Bar"

identical(v1, v2)
#> [1] TRUE

v2[2]
#> <toy_vector[1]>
#> [1] "World is Bar"

Created on 2021-01-23 by the reprex package (v0.3.0)

Note that we didn't need to create a coercion method, in this case the default coercion method for records is good enough.

moodymudskipper
  • 46,417
  • 11
  • 121
  • 167
1

Add an explicit `[.toy_vector` which subsets the descriptor attribute.

Like this:

`[.toy_vector` <- function(x,i){
      new_toy_vector(vec_data(NextMethod()),
                     descriptor = attr(NextMethod(), "descriptor")[i])
    }

I'm not sure how to get attributes to 'subset' in this way using vctrs, or even if it's possible. But using this method we can basically do what vctrs does, and then some.

Bear in mind that subsetting generic will no longer call the `[.vctrs_vctr` method, so you'll lose other vctrs functionallity (such as subsetting sub-classes with vec_restore()) and may need to implement further fixes in the `[.toy_vector` method.

library(vctrs)

new_toy_vector <- function(
  x = character(),
  descriptor = character()) {
  
  vec_assert(x,character())
  vec_assert(descriptor, character())
  
  new_vctr(x,
           descriptor = descriptor,
           class = "toy_vector")
}



format.toy_vector <- function(x, ...) {
  paste0(vec_data(x)," is ", attr(x, "descriptor"))
}

obj_print_data.toy_vector <- function(x) {
  cat(format(x), sep = "\n")
}


vec_ptype2.toy_vector.toy_vector <- function(x, y, ...) {
  new <- c(attr(x, "descriptor"), attr(y, "descriptor"))
  new_toy_vector(descriptor = new)
}

vec_cast.toy_vector.toy_vector <- function(x, to, ...) {
  new_toy_vector(vec_data(x),
                 attr(to, "descriptor"))
}

`[.toy_vector` <- function(x,i){
  new_toy_vector(vec_data(NextMethod()),
                 descriptor = attr(NextMethod(), "descriptor")[i])
}

c(new_toy_vector("Hello", "Foo"), new_toy_vector("World", "Bar")) -> tmp

tmp
#> <toy_vector[2]>
#> Hello is Foo
#> World is Bar

tmp[1]
#> <toy_vector[1]>
#> Hello is Foo

tmp[2]
#> <toy_vector[1]>
#> World is Bar

Created on 2021-01-19 by the reprex package (v0.3.0)

Captain Hat
  • 2,444
  • 1
  • 14
  • 31
  • 1
    This is a good hotfix that can also be extended to subassignment, double bracket subsetting and double bracket subassignment (+1). At that point you might as well work with plain S3 instead of the vctrs package. I was hoping to avoid this and have a solution within the vctrs framework. – teunbrand Jan 19 '21 at 13:49
0

I tried your code and I got a more informative error message:

Error: Can't combine `..1` <toy_vector> and `..2` <toy_vector>.
x Some attributes are incompatible.
ℹ The author of the class should implement vctrs methods.
ℹ See <https://vctrs.r-lib.org/reference/faq-error-incompatible-attributes.html>.
Run `rlang::last_error()` to see where the error occurred.

https://vctrs.r-lib.org/reference/faq-error-incompatible-attributes.html

If you go to the page about the error, the answer is there: vctrs does not know by default how to combine custom attributes. Your vectors have different attributes: Foo and Bar.

If you try

a <- new_toy_vector("Hello", "Foo")
b <- new_toy_vector("World", "Foo")
c(a, b)

this will work.

fvall
  • 380
  • 2
  • 9
  • Thank you for taking the time to answer the question, I appreciate that. Unfortunately, your suggestion does not address the preservation of attributes parallel to the vector. – teunbrand Jan 18 '21 at 18:31
0

To provide some context why I put a bounty on this question (and to give a bad answer to the question); I can get concatenation to work, but this causes trouble in other areas. So obviously something isn't right, but what?

library(vctrs)

new_toy_vector <- function(
  x = character(),
  descriptor = character()) {
  
  vec_assert(x,character())
  vec_assert(descriptor, character())
  
  new_vctr(x,
           descriptor = descriptor,
           class = "toy_vector")
}



format.toy_vector <- function(x, ...) {
  paste0(vec_data(x)," is ", attr(x, "descriptor"))
}

obj_print_data.toy_vector <- function(x) {
  cat(format(x), sep = "\n")
}


vec_ptype2.toy_vector.toy_vector <- function(x, y, ...) {
  new <- c(attr(x, "descriptor"), attr(y, "descriptor"))
  new_toy_vector(descriptor = new)
}

vec_cast.toy_vector.toy_vector <- function(x, to, ...) {
  new_toy_vector(vec_data(x),
                 attr(to, "descriptor"))
}

z <- c(new_toy_vector("Hello", "Foo"), new_toy_vector("World", "Bar"))
print(z)
#> <toy_vector[2]>
#> Hello is Foo
#> World is Bar

# Subsetting doesn't work properly
z[2]
#> <toy_vector[1]>
#> World is Foo
#> World is Bar

Created on 2021-01-18 by the reprex package (v0.3.0)

teunbrand
  • 33,645
  • 4
  • 37
  • 63
  • 1
    Not sure what the fix is, but the problem is that your 'descriptor' attribute is not being treated as subsettable in the way that, for instance the 'names' attribute would be in base R. That is, the attributes of `example[1]` are identical to those of `example` – Captain Hat Jan 19 '21 at 12:19
  • 1
    Yes this makes sense to me. I also don't know how and with what function to adress this to remain in the vctrs framework. One could of course implement a `[.toy_vector` method (along with `[<-.toy_vector`, `[[.toy_vector` and `[[<-.toy_vector`), but at that point it doesn't make much sense to use the vctrs in the first place. – teunbrand Jan 19 '21 at 12:31
  • Is a vector style s3 object the best thing to use for this use-case? If every element of the vector is associated with two different values, maybe you could build a new data_frame class? I'm not aware of anything in R which subsets attributes apart from `names()`, which is a primitive. – Captain Hat Jan 19 '21 at 14:15
  • 1
    The `vctrs_rcrd` class sort of does what you suggest, except it wraps a list with equal length elements instead of a data.frame for efficiency reasons. Bioconductor packages often implement parallel attribute classes, but in S4 based on the `S4Vectors` package. The reason I'm interested in this problem is because I want an expression as a parallel attribute to a vctrs vector, which doesn't fit with the `vctr_rcrd` class because an expression is technically not a vector (see also https://stackoverflow.com/q/65777709/11374827). – teunbrand Jan 19 '21 at 14:23