S7 Classes for Designs and Results

Object-oriented programming in R with S7

Published

March 13, 2026

R offers several object‑oriented systems (S3, S4, R6), and S7 is the most recent addition: a modern, lightweight, and more predictable approach to defining classes and methods. For statistical workflows—especially those involving complex design objects, parameter grids, and multi‑software comparisons—S7 brings a clean and explicit structure. In this post, I walk through how I use S7 to define two core objects in my sample‑size comparison pipeline:

See the official S7 Documentation


Designs: ssc_design

For each type of design, we compute a parameter grid (see the pipeline) that we store in a list called params.

Our goal is to define a class that represents a design object containing all the information required for cross‑software comparisons.

Properties

We start by creating a new class using S7::new_class, specifying the properties we want in each object:

ssc_design <-
  new_class(
    "ssc_design",
    # PROPERTIES
    properties = list(
      endpoint = class_character, # survival or binary
      type = class_character, # fixed or gs
      params = class_list, # grid
      computation = new_property(class = class_character, default = NULL)
    )
)

The design object contains:

  • endpoint: "binary" or "survival", depending on the type of endpoint.
  • type: "fixed" or "gs" (group‑sequential).
  • params: a list storing the grid of parameters used for the design.
  • computation: for binary endpoints, this specifies whether the test is "exact", "pooled", or "unpooled". It should be NULL for survival designs.

Constructor

At this stage, only the property types are defined. Next, we define how the object is constructed by writing a constructor:

Code
# CONSTUCTOR
constructor = function(
  endpoint = c("binary", "survival"),
  type = c("fixed", "gs"),
  params,
  computation = character(0)
) {
  endpoint <- arg_match(endpoint)
  type <- arg_match(type)
  new_object(
    S7_object(),
    endpoint = endpoint,
    type = type,
    params = params,
    computation = computation
  )
}

The constructor controls how a ssc_design object is created and restricts the possible argument values.

We also want to enforce consistency rules. For example:

  • Binary endpoints must specify a computation method.
  • Survival endpoints must not specify a computation method.
  • The params list must contain at least $list and $table.

Validator

We encode these rules in a validator:

Code
validator = function(self) {
  if (self@endpoint == "binary") {
    if (
      length(self@computation) == 0 || 
        !(self@computation %in% c("exact", "pooled", "unpooled"))
    ) {
      "@computation should be one of exact, pooled or unpooled."
    }
  } else if (length(self@computation) != 0) {
    "@computation should be NULL for survival endpoint"
  } else if (length(self@params$list) == 0 || length(self@params$table) == 0) {
    "@params should contains at least $list and $table."
  }
}

The validator is automatically executed when building a ssc_design object, if the object violates any rule, an error is returned.

Example: the following call triggers a validation error because "poled" is not a valid computation method:

Code
ssc_design(
  endpoint = "binary",
  type = "fixed",
  params = params,
  computation = "poled"
)
Code
Error:
! <ssc_design> object is invalid:
- @computation should be one of exact, pooled or unpooled.

Methods

A main benefit of defining classes is that you can attach methods to them.
Below, we create a print method for ssc_design. We also create a helper method show_params():

Code
# Method to print params
method(show_params, ssc_design) <- function(x) {
  params <- x@params
  params_list <- map(
    params$list,
    ~ str_flatten(., collapse = ", ", last = " and ")
  )
  cat("Moving parameters:\n")
  cli::cli_dl(params_list)
  cat("\nAdditional parameters:\n")
  cli::cli_dl(params$additional)
  cat("\nNumber of parameters combinaisons:\n")
  print(nrow(params$table))
}

# Print method
method(print, ssc_design) <- function(x) {
  cli::cli_h2("SSC {x@endpoint} {x@type} design")
  if (length(x@computation) != 0) {
    cli::cli_alert_info("{.var {x@computation}} computation.")
  }
  cli::cli_par()
  show_params(x)
}

Example:

Code
ssc_design(
    endpoint = "binary",
    type = "fixed",
    params = params,
    computation = "pooled"
)
Code
── SSC binary fixed design ──

ℹ `pooled` computation.
Moving parameters:
alpha: 0.01, 0.05, 0.1, 0.2 and 0.49
power: 0.51, 0.8, 0.9 and 0.99
pi_c: 0.1, 0.3, 0.5, 0.8 and 0.9
delta_pi: 0.05, 0.15, 0.25 and 0.49

Additional parameters:
sided: 2

Number of parameters combinaisons:
[1] 300

Results: ssc_results

For each design and each software or R package, we want a class that stores the computed sample sizes.

Properties

The ssc_results class contains:

  • tbl: a tibble storing the sample size results,
  • design: the associated ssc_design object,
  • method: the method or software used (e.g. "rpact", "nQuery").
Code
ssc_results <-
  new_class(
    "ssc_results",
    # PROPERTIES
    properties = list(
      tbl = class_list, # result table
      design = ssc_design, # design
      method = class_character # rpact, nQuery ...
    )
  )

Validator

The validator uses the design’s parameter table to ensure:

  • the number of results does not exceed the number of parameter combinations,
  • there are no duplicated parameter sets,
  • all parameter combinations in the results appear in the design’s parameter grid.
Code
# VALIDATOR
validator <- function(self) {
  if (nrow(self@tbl) > nrow(self@design@params$table)) {
    return("@tbl has too many rows according to the parameters.")
  }
  params_names <- names(self@design@params$list)
  cond_duplicate <- 
    self@tbl |>
    select(all_of(params_names)) |>
    duplicated() |>
    any()
  if (cond_duplicate) {
    return("@tbl should not contain any duplicate of input combinations.")
  }
  cond_in_params <-
    self@tbl |> 
    anti_join(self@design@params$table, by = params_names) |> 
    nrow()
  if (cond_in_params > 0) {
    return("@tbl should not contain combinations not present in @params$table.")
  }
  return(NULL)
}

Methods

We can now instantiate and inspect a results object:

Code
rpact_results <- ssc_results(
    tbl = rpact_table_results
    design = design_bin_fixed_pooled,
    method = "rpact"
)

Printing:

Code
print(rpact_results)
Code
── SSC rpact results for binary fixed design ──

# A tibble: 300 × 5
   alpha power  pi_c delta_pi     n
   <dbl> <dbl> <dbl>    <dbl> <dbl>
 1  0.01  0.51   0.1     0.05  1184
 2  0.05  0.51   0.1     0.05   690
 3  0.1   0.51   0.1     0.05   488
 4  0.2   0.51   0.1     0.05   299
 5  0.49  0.51   0.1     0.05    90
 6  0.01  0.8    0.1     0.05  2041
 7  0.05  0.8    0.1     0.05  1372
 8  0.1   0.8    0.1     0.05  1080
 9  0.2   0.8    0.1     0.05   788
10  0.49  0.8    0.1     0.05   410
# ℹ 290 more rows
# ℹ Use `print(n = ...)` to see more rows

Summary:

Code
summary(rpact_results)
Code
── SSC rpact results for binary fixed design ──

     alpha          power             pi_c           delta_pi            n         
 Min.   :0.01   Min.   :0.5100   Min.   :0.1000   Min.   :0.0500   Min.   :   2.0  
 1st Qu.:0.05   1st Qu.:0.7275   1st Qu.:0.1000   1st Qu.:0.0500   1st Qu.:  54.0  
 Median :0.10   Median :0.8500   Median :0.3000   Median :0.1500   Median : 192.5  
 Mean   :0.17   Mean   :0.8000   Mean   :0.4067   Mean   :0.2047   Mean   : 832.8  
 3rd Qu.:0.20   3rd Qu.:0.9225   3rd Qu.:0.5000   3rd Qu.:0.2500   3rd Qu.: 875.8  
 Max.   :0.49   Max.   :0.9900   Max.   :0.9000   Max.   :0.4900   Max.   :9578.0 

Display the design parameters directly from the results object:

Code
show_params(rpact_results)
Code
Moving parameters:
alpha: 0.01, 0.05, 0.1, 0.2 and 0.49
power: 0.51, 0.8, 0.9 and 0.99
pi_c: 0.1, 0.3, 0.5, 0.8 and 0.9
delta_pi: 0.05, 0.15, 0.25 and 0.49

Additional parameters:
sided: 2

Number of parameters combinaisons:
[1] 30