Skip to main content
View

How to interoperate with R6, S4, and Reference Classes

TypR does not replace R's OOP systems — it works alongside them. This guide shows how to use R6, S4, and RC objects from TypR code.

The key concept: Foreign<T>​

R6, S4, and RC objects are opaque R values. TypR represents them with Foreign<Any>, a type that passes through the compiler untouched — no field access, no method calls, just values flowing through bindings and function arguments.

type R6Person <- Foreign<Any>;

This is not a limitation. It is the design: TypR does not try to model R's dynamic OOP systems inside its type checker. Instead, you wrap the OOP object in Foreign<Any> and delegate operations to R code.

Working with R6 objects​

Suppose you have an R6 class in R/Person.R:

Person <- R6::R6Class("Person",
  public = list(
    name = NULL,
    age = NULL,
    initialize = function(name, age) {
      self$name <- name
      self$age <- age
    },
    greet = function() {
      paste0("Hi, I'm ", self$name)
    }
  )
)

From TypR, declare the constructor and methods as signatures:

type R6Person <- Foreign<Any>;

@extern Person$new: (name: char, age: int) -> R6Person;

let p <- Person$new("Alice", 30);

For calling methods, use an extern escape hatch since method call syntax ($) is not typed:

extern (p: R6Person) -> char r#"p$greet()"#;

The r#"..."# raw R string is emitted verbatim into the generated .R file.

Working with S4 objects​

S4 classes follow the same pattern — wrap the object in Foreign<Any> and use @extern for the generic functions:

type S4Model <- Foreign<Any>;

@readRDS: (path: char) -> S4Model;
@extern stats::coef: (object: S4Model) -> Foreign<Any>;
@extern stats::summary: (object: S4Model) -> Foreign<Any>;

let m: S4Model <- readRDS("model.rds");
let s <- summary(m);

For creating S4 objects, use @extern with the constructor:

@extern methods::new: (Class: char, ...args: Any) -> Foreign<Any>;

Working with Reference Classes (RC)​

RC objects are handled identically to R6 — they are just R values:

type RCLogger <- Foreign<Any>;

@extern Logger$new: () -> RCLogger;

let log <- Logger$new();

Using R {} blocks for complex OOP interactions​

When you need to call multiple methods or access fields, R {} blocks are often cleaner than chaining extern escape hatches:

let result <- R {
  person <- Person$new("Bob", 25)
  paste(person$name, "is", person$age)
};

This block is emitted as-is into the generated R code. It is not type-checked, but it integrates seamlessly with the surrounding typed code.

Best practices​

  1. Wrap, do not rebuild — Use Foreign<Any> for OOP objects. TypR is not meant to reimplement R's class systems.

  2. Signature the entry points — Declare @extern for constructors and key methods. Leave internal field access to R {} blocks.

  3. Keep OOP code in R files — If a class is complex, define it in R/ and only call it from TypR via signatures. This is the cleanest interop.

  4. Prefer R {} for multi-step OOP — When you need to create, configure, and call methods on an object in sequence, a raw R block is simpler than multiple extern escape hatches.

Full example​

# Type declarations
type R6Person <- Foreign<Any>;
@extern Person$new: (name: char, age: int) -> R6Person;

# Create and use
let p <- Person$new("Alice", 30);

# Multi-step interaction via R block
let greeting <- R {
  p$greet()
};

Where to go next​