drJPK
drJPK

Reputation: 1

Trying to write an S4 data class which has lists of lists in the slots. How is this nesting in slots meant to be done?

I am trying to create a simple S4 class that allows the storage of all necessary parameters to generate a semi-deteministic data synthetic set for another set of functions to act on.

Ultimately, two functions will be called in my R package:

getDataModel(x) and setDataModel(...)

setDataModel(...) will (to begin with) interactively ask the user for the parameters to define the data generation model (perhaps structure is a better term?) getDataModel(x) will read and parse the model object for use by other generation functions that actually spit out the final data. I think my problem is that I may be trying to over-modularise my code.

Consider this S4 Class

setClass(
  "TimefitteR_Param_List",
  slots = list(
    intercept_offset = "numeric",
    intercept_noise = "numeric",
    slope_offset = "numeric",
    slope_noise = "numeric",
    curvature_offset = "numeric",
    curvature_noise = "numeric"
  ),
  prototype = list(
    intercept_offset = 1,
    intercept_noise = 1,
    slope_offset = 0,
    slope_noise = 1,
    curvature_offset = 0,
    curvature_noise = 1
  )
)

This works exactly as intended and creates a 6 value named list when called by itself.

However, the following class fails gloriously when I call document() or load_all()

#' @include classes_param_model.R
setClass(
  "TimefitteR_Data_Model",
  slots = list(
    vars = "list",
    time_params = "list",
    base_params = "TimefitteR_Param_List",
    conditions_params = "list",
    predictors_params = "list"
  ),
  prototype = list(
    vars = list(
      time = "Time",
      conditions = list(class="factor", levels = c("Control", "Intervention")),
      predictors = list(
        Gender = list(class="factor", levels = c("Female","Male")),
        SES = list(class="factor", levels = c("Low","Moderate","High"))
      )
    ),
    time_params = list("from"=0,"length"=20,"step"=1),
    base_params = "TimefitterR_Param_List",
    conditions_params = "TimefitteR_Param_List",
    predictors_params = list(
      Gender = list("TimefitteR_Param_List"),
      SES = list("TimefitteR_Param_List","TimefitteR_Param_List")
    )
  ),
  contains = "TimefitteR_Param_List"
)
! Failed to load R/classes_data_model.R
Caused by error in `makePrototypeFromClassDef()`:
! in making the prototype for class “TimefitteR_Data_Model” elements of the prototype failed to match the corresponding slot class: base_params (class "TimefitteR_Param_List" ), conditions_params (class "list" )

What I am after is an object (say mod) that has this structure

mod
  vars
    time <<character>>
    conditions <<factor with levels "Control" and "Intervention" by default>>
    predictors <<list could be any length including NULL>>
      Gender <<factor with levels "Female" & "Male" by default>>
      SES <<factor with levels "Low", "Moderate", & "High" by default>>
  time_params <<list of 3>>
    from <<numeric default 0>>
    length <<numeric default 20>>
    step <<numeric default 1>>
  base_params <<TimefitteR_Param_List>>
  conditions_params <<list of length(conditions-1) with each element a TimefitteR_Param_List>>
  conditions_params <<list of length(predictors-1) with each element a named list>>
    Gender <<list of length(Gender-1) with each element a TimefitteR_Param_List>>
    SES <<list of length(SES-1) with each element a TimefitteR_Param_List>>

I can (probably) work out how to build this data structure as a return object (S3 object??) from a call to setDataModel(...) but I was hoping to use an S4 class and its prototype system to build this all in one from a simple call to new("TimefitteR_Data_Model")

I think (know) the problem is the fact that I have lists of lists in the prototype syntax and simple un-nested lists in the slots. BUT,...

  1. how am I supposed to manage the nesting?
  2. Is the preferred solution to create classes for each of the sub levels type structures and then @include those?
  3. Am I overthinking this and missing something really obvious?

Any help is appreciated.

Upvotes: 0

Views: 48

Answers (1)

Mikael Jagan
Mikael Jagan

Reputation: 11336

For slots designed as nested lists, you need to define validity methods checking that the length and components meet your requirements. How rigorously you check is up to you ...

You can draw inspiration from a simpler example like this one:

setClass("param",
         slots = c(a = "numeric", b = "numeric"),
         prototype = list(a = 0, b = 1))
setClass("listOfParam",
         contains = "list",
         validity = function(object) {
             class. <- getClassDef("param")
             for (elt in object)
                 if (!is(elt, class.))
                     return("component is not a \"param\" object")
             TRUE
         })
setClass("model",
         slots = c(simpleSlot = "param", nestedSlot = "listOfParam", anotherSlot = "numeric"),
         validity = function(object) {
             if (length(object@nestedSlot) != length(object@anotherSlot))
                 return("slots 'nestedSlot' and 'anotherSlot' do not have equal length")
             TRUE
         })

It's not strictly necessary to define "wrapper" classes like listOfParam. The above could have been implemented as:

setClass("param",
         slots = c(a = "numeric", b = "numeric"),
         prototype = list(a = 0, b = 1))
setClass("model",
         slots = c(simpleSlot = "param", nestedSlot = "list", anotherSlot = "numeric"),
         validity = function(object) {
             class. <- getClassDef("param")
             for (elt in object@nestedSlot)
                 if (!is(elt, class.))
                     return("component of slot 'nestedSlot' is not a \"param\" object")
             if (length(object@nestedSlot) != length(object@anotherSlot))
                 return("slots 'nestedSlot' and 'anotherSlot' do not have equal length")
             TRUE
         })

Upvotes: 1

Related Questions