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

Constructors: Using rlang::inject with new_object() #497

Open
mccarthy-m-g opened this issue Nov 19, 2024 · 3 comments
Open

Constructors: Using rlang::inject with new_object() #497

mccarthy-m-g opened this issue Nov 19, 2024 · 3 comments

Comments

@mccarthy-m-g
Copy link

Is there a way to use rlang::inject (or something similar) with new_object? It currently throws an error that new_object() must be called from within a constructor. Here's a reprex:

library(S7)
library(rlang)
library(purrr)
library(palmerpenguins)

# My real use case involves JSON data, so we'll transform into a list. 
my_penguin <- as.list(penguins[1,])

penguin <- new_class(
  "penguin",
  properties = list(
    species = class_factor,
    island = class_factor,
    bill_length_mm = class_double,
    bill_depth_mm = class_double,
    flipper_length_mm = class_integer,
    body_mass_g = class_integer,
    sex = class_factor,
    year = class_integer
  ),
  constructor = function(x) {
    # The `penguins` data frame contains all the possible properties for `penguin` objects.
    # For my real use case, these properties would be read from a JSON Schema file.
    penguins_properties <- set_names(colnames(penguins))
    # I want to pluck the property values for `my_penguin` as a named list that can be used
    # to construct the object.
    out <- map(penguins_properties, function(y) pluck(x, y))
    # This throws an error.
    inject(new_object(S7_object(), !!!out))
  }
)

penguin(my_penguin)
#> Error in new_object(S7_object(), species = structure(1L, levels = c("Adelie", : `new_object()` must be called from within a constructor

Created on 2024-11-18 with reprex v2.1.1

@mccarthy-m-g
Copy link
Author

As an aside, is there a way to access the property names for an object within the constructor function?

prop_names() exists for objects in the environment, but being able to access property names from within the constructor function would be a useful pattern. For example, in the reprex above it would mean we could do something like:

penguins_properties <- access_prop_names(prop_names_from_penguin_object_properties)

Instead of relying on referencing an external object (the penguins data frame) that also contains the property names.

@t-kalinowski
Copy link
Member

Thanks for reporting! I agree we'll want to enable this use case.

Just so you're not blocked, here is an approach you can use with the current release:

library(S7)
library(rlang)
library(purrr, warn.conflicts = FALSE)

my_penguin <- as.list(palmerpenguins::penguins[1, ])

Penguin <- new_class(
  "Penguin",
  properties = list(
    species = class_factor,
    island = class_factor,
    bill_length_mm = class_double,
    bill_depth_mm = class_double,
    flipper_length_mm = class_integer,
    body_mass_g = class_integer,
    sex = class_factor,
    year = class_integer
  ),
  constructor = function(x) {

    Penguin <- sys.function() # Get reference to Self / Class
    prop_vals <- map(set_names(names(Penguin@properties)),
                     function(prop_name) pluck(x, prop_name))

    # hack to enable using `new_object()` with NSE.
    constructor <- function() {}
    body(constructor) <- inject(quote(new_object(S7_object(), !!!prop_vals)))
    attributes(constructor) <- attributes(sys.function())
    constructor()
  }
)

Penguin(my_penguin)
#> <Penguin>
#>  @ species          : Factor w/ 3 levels "Adelie","Chinstrap",..: 1
#>  @ island           : Factor w/ 3 levels "Biscoe","Dream",..: 3
#>  @ bill_length_mm   : num 39.1
#>  @ bill_depth_mm    : num 18.7
#>  @ flipper_length_mm: int 181
#>  @ body_mass_g      : int 3750
#>  @ sex              : Factor w/ 2 levels "female","male": 2
#>  @ year             : int 2007

Created on 2024-11-19 with reprex v2.1.1

@mccarthy-m-g
Copy link
Author

Thanks for the workaround! I was able to apply it to my real use case.

Just for reference to anyone else reading this, here's what you'd do if you still want to rely on referencing an external object (the penguins data frame) to get the property names:

library(S7)
library(rlang)
library(purrr)
library(palmerpenguins)

my_penguin <- as.list(penguins[1,])

penguin <- new_class(
  "penguin",
  properties = list(
    species = class_factor,
    island = class_factor,
    bill_length_mm = class_double,
    bill_depth_mm = class_double,
    flipper_length_mm = class_integer,
    body_mass_g = class_integer,
    sex = class_factor,
    year = class_integer
  ),
  constructor = function(x) {
    props <- set_names(colnames(penguins)) # Get prop names from `penguins` tbl
    prop_vals <- map(props, function(prop_name) pluck(x, prop_name))

    # hack to enable using `new_object()` with NSE.
    constructor <- function() {}
    body(constructor) <- inject(quote(new_object(S7_object(), !!!prop_vals)))
    attributes(constructor) <- attributes(sys.function())
    constructor()
  }
)

penguin(my_penguin)
#> <penguin>
#>  @ species          : Factor w/ 3 levels "Adelie","Chinstrap",..: 1
#>  @ island           : Factor w/ 3 levels "Biscoe","Dream",..: 3
#>  @ bill_length_mm   : num 39.1
#>  @ bill_depth_mm    : num 18.7
#>  @ flipper_length_mm: int 181
#>  @ body_mass_g      : int 3750
#>  @ sex              : Factor w/ 2 levels "female","male": 2
#>  @ year             : int 2007

Created on 2024-11-19 with reprex v2.1.1

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