4

I'm trying to make a vector class using the vctrs package, that stores an expression. Mainly because I want to use it in a different vctrs vector. An expression is not a vector type, so a naive implementation of a vector expression (named vexpr here) fails.

library(vctrs)

expr <- scales::math_format()(1:10)

new_vexpr <- function(x) {
  new_vctr(x, class = 'vexpr')
}

new_vexpr(expr)
#> Error: `.data` must be a vector type.

So, I thought, maybe I can implement the expression itself as an attribute in parallel with the vector.

new_vexpr <- function(x) {
  if (!is.expression(x)) {
    stop()
  }
  new_vctr(seq_along(x),
           expr = x,
           class = "vexpr")
}

format.vexpr <- function(x, ...) {
  ifelse(is.na(vec_data(x)), NA, format(as.list(attr(x, "expr"))))
}

# Works!
x <- new_vexpr(expr)

I quickly began running into trouble because I haven't implemented the boilerplate vec_ptype2() and vec_cast() methods yet.

# Looks like it might work
c(x, x)
#> <vexpr[20]>
#>  [1] 10^1L  10^2L  10^3L  10^4L  10^5L  10^6L  10^7L  10^8L  10^9L  10^10L
#> [11] 10^1L  10^2L  10^3L  10^4L  10^5L  10^6L  10^7L  10^8L  10^9L  10^10L

# Expression not concatenated (as might be expected)
attr(c(x, x), "expr")
#> expression(10^1L, 10^2L, 10^3L, 10^4L, 10^5L, 10^6L, 10^7L, 10^8L, 
#>     10^9L, 10^10L)

So I tried to implement the boilerplate methods.

vec_ptype2.vexpr.vexpr <- function(x, y, ...) {
  new <- c(attr(x, "expr"), attr(y, "expr"))
  new_vctr(integer(0), expr = new, class = "vexpr")
}

vec_cast.vexpr.vexpr <- function(x, to, ...) {
  new_vctr(vec_data(x), expr = attr(to, "expr"),
           class = "vexpr")
}

Which helped concatenating the vectors, but return erroneous subsetting results.

# Expression is concatenated!
attr(c(x, x), "expr")
#> expression(10^1L, 10^2L, 10^3L, 10^4L, 10^5L, 10^6L, 10^7L, 10^8L, 
#>     10^9L, 10^10L, 10^1L, 10^2L, 10^3L, 10^4L, 10^5L, 10^6L, 
#>     10^7L, 10^8L, 10^9L, 10^10L)

# Subsetting doesn't make sense, should be 10^2L
x[2]
#> <vexpr[1]>
#> [1] 10^1L

# Turns out, full expression still there
attr(x[2], "expr")
#> expression(10^1L, 10^2L, 10^3L, 10^4L, 10^5L, 10^6L, 10^7L, 10^8L, 
#>     10^9L, 10^10L)

Fine, so I defined my own subsetting methods outside the vctrs system, which initially seemed to work.

# Define S3 subsetting method
`[.vexpr` <- function(x, i, ...) {
  expr <- attr(x, "expr")
  ii <- vec_as_location(i, length(expr), names = names(x),
                        missing = "propagate")
  
  new_vctr(vec_data(x)[ii],
           expr = expr[ii],
           class = "vexpr")
}

# Subsetting works!
x[2]
#> <vexpr[1]>
#> [1] 10^2L

# Seemingly sensible concatenation
c(x[2], NA)
#> <vexpr[2]>
#> [1] 10^2L <NA>

But also started to generate nonsensical results.

# expr is duplicated? would have liked the 2nd expression to be `expression(NA)`
attr(c(x[2], NA), "expr")
#> expression(10^2L, 10^2L)

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

Obviously, I'm doing something wrong here, but I haven't succeeded in debugging this problem. I have also tried implementing the vec_restore() method for vexpr, but this confused me even more. Have you seen nice implementations of parallel attribute vctrs somewhere? Do you know what I might be doing wrong?

Related question here: How do I build an object with the R vctrs package that can combine with c() (concatenating vctrs with attributes)

Related discussion here: https://github.com/r-lib/vctrs/issues/559

EDIT: I'm not married to the parallel attribute idea. If vec_data(x) would be an index into attr(x, "expr") that would work too, but I haven't managed this either.

EDIT2: Wrapping the expression in a list of calls seems to fit everything tidily. However, I'd still be interested in the parallel attribute / index attribute stability. Example of list wrapping (seemingly all methods work as they should!):

new_vexpr <- function(x) {
  if (!is.expression(x)) {
    x <- as.expression(x)
    if (!is.expression(x)) {
      stop()
    }
  }
  x <- as.list(x)
  new_vctr(x,
           class = "vexpr")
}

as.expression.vexpr <- function(x) {
  do.call(expression, vec_data(x))
}
teunbrand
  • 33,645
  • 4
  • 37
  • 63

1 Answers1

1

You could wrap your expression in a list :

library(vctrs)

expr <- scales::math_format()(1:10)

new_vexpr <- function(x) {
  new_vctr(list(x), class = 'vexpr')
}

res <- c(new_vexpr(expr), new_vexpr(expr))
res
#> <vexpr[2]>
#> [1] expression(10^1L, 10^2L, 10^3L, 10^4L, 10^5L, 10^6L, 10^7L, 10^8L, ,     10^9L, 10^10L)
#> [2] expression(10^1L, 10^2L, 10^3L, 10^4L, 10^5L, 10^6L, 10^7L, 10^8L, ,     10^9L, 10^10L)

res[2]
#> <vexpr[1]>
#> [1] expression(10^1L, 10^2L, 10^3L, 10^4L, 10^5L, 10^6L, 10^7L, 10^8L, ,     10^9L, 10^10L)

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

moodymudskipper
  • 46,417
  • 11
  • 121
  • 167
  • Yes that also occurred to me (see yesterday's edit) and it solved my current problem perfectly (+1). I see the question as somewhat broader than just solving the expression case though, think S4 or R6 attributes or other data abstractions (run-length encodings for an example) that you could consider vector like but not coercible to a (plain) list. I know `?vctrs::vec_proxy` documentation states that these things are 'partially supported', but what part exactly is supported and what part do I need to add myself to get this to work? – teunbrand Jan 21 '21 at 18:26
  • But we don't coerce to a list here though, we wrap into one, so this could work with any object type couldn't it ? I saw your bountied question and I believe using records is really the way to go. I believe you could work around it by setting a different attribute for each element (but I don't think it's worth the trouble when you have records), else you could define your own method for `[`, and maybe `[[`. – moodymudskipper Jan 21 '21 at 19:05
  • ah, and I had missed your edit indeed, guilty of not reading to the end sorry :) – moodymudskipper Jan 21 '21 at 19:07
  • To be fair, I had also failed to notice that you wrap the entire expression in a single list element. A small `tracemem()` experiment suggests that R doesn't make unnecessary copies upon concatenation/subsetting :). However, the ideal behaviour would be that if I subset `vexpr_obj[2]` it should return something equal to `new_vexpr(expr[2])`. I also totally agree that vctrs_rcrd is the way to go for atomic-like attributes. – teunbrand Jan 21 '21 at 19:19
  • By the way, if you illustrate a `vctrs_rcrd` example on the bountied question I'd totally give that bounty because I genuinely believe that is the best solution for that question. – teunbrand Jan 21 '21 at 19:27