Skip to main content

How to use raw R blocks in TypR

Not everything needs to be typed. TypR provides several escape hatches for writing plain R code when the type system gets in the way.

When to use raw R blocks

Use raw R when:

  • You need idiomatic R that is hard to type (pipes %>%, NSE, formulas ~)
  • You are calling complex dplyr/tidyr pipelines
  • You are working with R6/S4 objects and their methods
  • You are prototyping and want to move fast

The goal is not to type everything — it is to type the parts where types help.

R {} blocks — the primary escape hatch

let result <- R {
mtcars |>
dplyr::filter(cyl > 4) |>
dplyr::mutate(efficiency = mpg / wt) |>
dplyr::arrange(desc(efficiency))
};

R {} captures its body verbatim and emits it as a plain R block { ... }. It is the preferred form for idiomatic R that is difficult to type.

Using return values

R {} blocks evaluate to the value of their last instruction, just like R blocks:

let count <- R { length(x) };         # returns an integer
let names <- R { colnames(df) }; # returns a character vector

The return value flows into the surrounding typed code as Foreign<Any>.

Calling typed functions from R blocks

You can call TypR functions from within R {} blocks — the transpiler resolves the references:

let compute <- fn(x: int): int { x * 2 };

let result <- R {
sapply(1:10, compute)
};

extern — typed R functions

When you want type checking on the signature but have a plain R body:

extern (x: int, y: char) -> char r#"paste0(x, y)"#;

The compiler checks the types of x and y and the return type. The R code itself is emitted verbatim. This is useful for small helpers where you want the type safety but not the effort of writing typed TypR.

function() — untyped R functions

Any function(...) expression in TypR is captured as an untyped R function:

let my_func <- function(x, y) {
x + y
};

The body is not type-checked. This is useful for callbacks, callbacks to R functions that expect plain functions, and quick prototypes.

Comparison table

FormType-checked?Returns typed value?Best for
R {}NoForeign<Any>Idiomatic R pipelines, NSE
externSignature onlyYesSmall typed helpers with R bodies
function()NoRFunctionCallbacks, prototypes
@ signatureN/AN/ATyping existing R functions

Practical patterns

Data transformation pipeline

let clean_data <- fn(df: Foreign<Any>): Foreign<Any> {
R {
df |>
tidyr::drop_na() |>
dplyr::mutate(across(where(is.character), trimws))
}
};

Configuration with R code

let theme <- R {
ggplot2::theme_minimal() +
ggplot2::theme(
plot.title = ggplot2::element_text(size = 14, face = "bold"),
axis.text = ggplot2::element_text(size = 10)
)
};

Prototype first, type later

# Quick prototype (untyped)
let analyze <- function(data) {
summary(lm(mpg ~ wt, data = data))
};

# Later, add types
let analyze_typed <- fn(data: Foreign<Any>): Foreign<Any> {
R { summary(lm(mpg ~ wt, data = data)) }
};

Best practices

  1. Use R {} as the default escape — It is the cleanest way to embed R code. Save extern for cases where you need the typed signature.

  2. Keep R {} blocks small — Large R {} blocks defeat the purpose of using TypR. If a block grows beyond 10-15 lines, consider whether it should be a function in R/.

  3. Extract return types — Even if the block is untyped, you can annotate the binding with a type if the return value is known:

    let count: int <- R { nrow(df) };
  4. Do not nest R {} blocks — Keep them flat. If you need typed logic inside R code, call a TypR function from the R block.

Where to go next