Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

self-to-self coercion #1833

Open
RaymondBalise opened this issue Apr 22, 2023 · 7 comments
Open

self-to-self coercion #1833

RaymondBalise opened this issue Apr 22, 2023 · 7 comments

Comments

@RaymondBalise
Copy link

Hello vctrs folks,
One of my packages is getting an error which leads me here:
https://vctrs.r-lib.org/reference/faq-error-incompatible-attributes.html

When I google "self-to-self coercion methods" I only get your page. The object that is throwing the error is a tibble that has variables that have a "label" assigned to them (which is holding a brief name/explanation for each of thevariables). Can you please help me find something to read to explain how to "implement self-to-self coercion methods"?

Gratefully your,
Ray

@DavisVaughan
Copy link
Member

I assume the error is something like this?

library(vctrs)

x <- structure(1, label = "x1", class = "mylabelled")
y <- structure(2, label = "x2", class = "mylabelled")

vec_c(x, y)
#> Error in `vec_c()`:
#> ! Can't combine `..1` <mylabelled> and `..2` <mylabelled>.
#> ✖ 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>.

vctrs sees that both inputs have the same class and an attribute named label, but it can't combine these because it doesn't know what to do with the fact that label is different between x and y.

You'd have to write a vec_ptype2.mylabelled.mylabelled() method that either reconciles them in some way (like maybe taking the label of the LHS) or errors if the labels are different.

There is advice on implementing these methods here https://vctrs.r-lib.org/reference/howto-faq-coercion.html#implementing-vec-ptype-

@RaymondBalise
Copy link
Author

Thank you kindly Davis. That helps but I don't know enough to fix the problem.

Here is the reprex:

demo <- structure(
  list(
    record_id = c(1, 2, 3), 
    person = structure(
      c("Davis", "Davis", "Julia"), 
      label = "Most helpful person", 
      class = c("labelled", "character")
    ), 
    demo_complete = structure(
      c("Complete", "Incomplete", "Incomplete"), 
      label = "Complete?", 
      class = c("labelled", "character")
    )
  ), row.names = c(NA, -3L), class = "data.frame")

suppressPackageStartupMessages(library(dplyr))

demo |> 
  mutate(
    case_when(
      person == "Julia" ~ "Davis",
      TRUE ~ person
    )
  )
#> Error in `mutate()`:
#> ℹ In argument: `case_when(person == "Julia" ~ "Davis", TRUE ~ person)`.
#> Caused by error in `case_when()`:
#> ! Can't combine `"Davis"` <character> and `person` <labelled>.

#> Backtrace:
#>      ▆
#>   1. ├─dplyr::mutate(...)
#>   2. ├─dplyr:::mutate.data.frame(...)
#>   3. │ └─dplyr:::mutate_cols(.data, dplyr_quosures(...), by)
#>   4. │   ├─base::withCallingHandlers(...)
#>   5. │   └─dplyr:::mutate_col(dots[[i]], data, mask, new_columns)
#>   6. │     └─mask$eval_all_mutate(quo)
#>   7. │       └─dplyr (local) eval()
#>   8. ├─dplyr::case_when(person == "Julia" ~ "Davis", TRUE ~ person)
#>   9. │ └─dplyr:::vec_case_when(...)
#>  10. │   └─vctrs::vec_ptype_common(!!!everything, .ptype = ptype, .call = call)
#>  11. └─vctrs (local) `<fn>`()
#>  12.   └─vctrs::vec_default_ptype2(...)
#>  13.     ├─base::withRestarts(...)
#>  14.     │ └─base (local) withOneRestart(expr, restarts[[1L]])
#>  15.     │   └─base (local) doWithOneRestart(return(expr), restart)
#>  16.     └─vctrs::stop_incompatible_type(...)
#>  17.       └─vctrs:::stop_incompatible(...)
#>  18.         └─vctrs:::stop_vctrs(...)
#>  19.           └─rlang::abort(message, class = c(class, "vctrs_error"), ..., call = vctrs_error_call(call))

Created on 2023-04-26 with reprex v2.0.2

I think I need to add a function like this into my package (tidyREDCap):

vec_ptype2.character.labelled <- function(x, y, ...) {
  # browser()
  # thing <- x
  # labelVector::set_label(x, labelVector::get_label(x))
}

but I am at a loss for what to put inside the function because when I check the values of x and y with the browser() I see no values.

Can you please offer more advice.

@DavisVaughan
Copy link
Member

You would need vec_ptype2() and vec_cast() methods to be able to combine a character vector and a labelVector labelled object together, yes. You could study haven:::vec_ptype2.character.haven_labelled() and the other haven methods since they do a similar thing. I think the vignette I linked to above does a good job of discussing how to do this.

I see no values.

x and y should be empty vectors of the right type by the time they get to your method. The idea is that you should be able to figure out the common type between them based on the class and attributes alone. You don't need the data to do this.

If you don't own labelVector then I'd advise against adding the vctrs methods for it directly in your package, if you can help it.

@RaymondBalise
Copy link
Author

That code helped a lot! Below is what I came up with.

I think I have a fix in place. I wrote to the author of {labelVector}. With a touch of luck he will add in the missing methods. If you have some bandwidth, can you give it a look and offer tips/warnings/corrections?

demo <- structure(
  list(
    record_id = c(1, 2, 3), 
    person = structure(
      c("Davis", "Davis", "Julia"), 
      label = "Most helpful person", 
      class = c("labelled", "character")
    ), 
    demo_complete = structure(
      c("Complete", "Incomplete", "Incomplete"), 
      label = "Complete?", 
      class = c("labelled", "character")
    )
  ), row.names = c(NA, -3L), class = "data.frame")

suppressPackageStartupMessages(library(dplyr))

vec_ptype2.labelled.labelled <- function(x, y, ...) {
  x
}

vec_ptype2.character.labelled <- function(x, y, ...) {
  y
}

vec_cast.labelled.labelled <- function(x, to, ...) {
  if (identical(attributes(x), attributes(to))) {
    return(x)
  } else {
  cli::cli_abort(
    c(x = "You are trying to combine variables with different labels",
      "You can use tidyREDCap::drop_label() to erase one.")
  )
  }
}

vec_cast.character.labelled <- function(x, to, ...) {
  labelVector::set_label(x, labelVector::get_label(to))
}

vec_cast.labelled.character <- function(x, to, ...) {
  x
} 

result <- demo |> 
  mutate(
    new = 
    case_when(
      person == "Julia" ~ "Davis",
      TRUE ~ person
    )
  ) 

# View(result)

@RaymondBalise
Copy link
Author

Hello @DavisVaughan,
Using your advice we build functions to fix this bug. I wrote to the package author who made the problematic labeling function and he didn't write back. So we think we need to add the fix into our package. A sample of the functions we created is below. When I try to check the package I get complaints about undocumented functions.

❯ checking for missing documentation entries ... WARNING
  Undocumented code objects:
    ‘vec_cast.character.labelled’ ‘vec_cast.double.labelled’
    ‘vec_cast.integer.labelled’ ‘vec_cast.labelled.character’
    ‘vec_cast.labelled.double’ ‘vec_cast.labelled.integer’
    ‘vec_cast.labelled.labelled’ ‘vec_cast.labelled.logical’
    ‘vec_cast.logical.labelled’ ‘vec_ptype2.character.labelled’
    ‘vec_ptype2.double.labelled’ ‘vec_ptype2.integer.labelled’
    ‘vec_ptype2.labelled.labelled’ ‘vec_ptype2.logical.labelled’
  All user-level objects in a package should have documentation entries

When I look in Haven I only see the single export roxygen tags. What else should I do to to allow the package to pass checks? I noticed in Haven that these functions were listed as S3method(s) in the namespace. I don't know how to do that with roxygen. Any guidance would be greatly appreciated.

# char
#' @export
vec_ptype2.character.labelled <- function(x, y, ...) {
  y
}

# char
#' @export
vec_cast.character.labelled <- function(x, to, ...) {
  labelVector::set_label(x, labelVector::get_label(to))
}

# char
#' @export
vec_cast.labelled.character <- function(x, to, ...) {
  x
} 

@DavisVaughan
Copy link
Member

You're probably missing @import vctrs
https://github.com/tidyverse/haven/blob/47a4219dbc83a337453fd9622c51748df93aa299/R/haven-package.R#L7

Alternatively you can just @importFrom vctrs vec_cast and @importFrom vctrs vec_ptype2

Then @export will recognize vec_cast.labelled.character and friends as S3 methods and you wont need to document them

@RaymondBalise
Copy link
Author

Spot on. Thank you!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants