Skip to main content
View

The Whole Game

Before you start

This tutorial develops a small toy package from start to finish. The goal is the big picture and a workable workflow, before the rest of the documentation gets into the details of the type system.

It is a TypR adaptation of the chapter The Whole Game from R Packages (2e) by Hadley Wickham and Jennifer Bryan. The structure and the toy package are the same, so the two workflows can be compared side by side. All credit for the original material goes to the authors.

The tutorial is self-contained. Finishing it is not required to read on, but following along and actually building the package is strongly recommended.

Duration: 15 to 20 minutes.

Prerequisites​

  • R (≥ 4.1) installed
  • The typr compiler: see the installation guide
  • Git (optional, for the version-control section)
  • A terminal and a text editor

Run typr init once to install the R packages TypR needs (devtools, testthat):

typr init

Toy package: regexcite​

The package built here is small but has the features commonly seen in released packages:

  • Functions that address a specific need: helpers for work with regular expressions, made safer by TypR's type system.
  • Static type checking with typr check, a step that simply does not exist in a pure R workflow.
  • The established R workflows for installation, help, and quality checks:
    • Documentation via roxygen2, generated from type annotations.
    • Unit testing with testthat, extracted from Test blocks.
  • Version control with Git (optional but recommended).

The package is called regexcite, after the original chapter. The functions are deliberately simple; they only serve to demonstrate the development process. For actual regular-expression helpers, look at stringr, stringi or rex.

typr new​

typr new is TypR's equivalent of usethis::create_package(). Pick the location deliberately: somewhere alongside other projects, not nested inside another package or Git repo, and not inside an R package library.

typr new regexcite
Creating the R package 'regexcite'...
Writing DESCRIPTION
Writing NAMESPACE
Writing .Rbuildignore
Writing .gitignore
Writing TypR/main.ty
Writing tests/testthat.R
Writing README.md
Package R 'regexcite' successfully created!
Initializing renv...

Move into the new directory and look around:

cd regexcite
regexcite/
  DESCRIPTION       # package metadata
  NAMESPACE         # exports/imports (generated, do not edit)
  R/                # generated R code (do not edit by hand)
  TypR/
    main.ty         # typed source code, the business end
  tests/
    testthat.R      # standard testthat entry point
  man/              # documentation (auto-generated)
  README.md
  renv.lock         # environment snapshot
  rproj.Rproj       # RStudio project file

The layout is a standard R package with exactly one addition: the TypR/ folder, which holds the typed .ty sources. Everything under R/ is transpiled output and should never be edited directly.

renv today, Rig tomorrow

typr new currently initializes renv for environment and package management. A future release will switch to Rig, Posit's R installation manager, for this role. The package layout and the rest of this tutorial stay the same.

git init​

The regexcite directory is now a TypR source package. Making it a Git repository is optional but recommended, and it makes it easy to see what changes at each step:

git init
git add .
git commit -m "Initial commit: typr new scaffold"

The only change is a hidden .git directory. The scaffold already ships a .gitignore covering R's behind-the-scenes files.

Write the first function​

Splitting a single string into parts is a common task. Base R's strsplit() does this, but returns a list of character vectors, because it accepts a vector of strings. With just one string, the first element is always the one wanted:

x <- "alfa,bravo,charlie,delta"
strsplit(x, split = ",")
#> [[1]]
#> [1] "alfa"    "bravo"   "charlie" "delta"

The function str_split_one() captures this pattern and, on top of it, guarantees at compile time that one string goes in and a character vector comes out. Replace the contents of TypR/main.ty with:

# Split a single string into pieces.
@pub let str_split_one <- fn(x: char, pat: char): [char] {
  let parts <- strsplit(x, pat);
  parts[[1]] as! [char]
};

Two things to notice:

  • The signature fn(x: char, pat: char): [char] declares that both arguments are single strings and the result is a character vector. @pub exports the function from the package (it maps to roxygen2's @export).
  • strsplit is an R function TypR types at the T3 tier, so its return type is any. The explicit cast as! [char] is the programmer asserting "this is a character vector". See escape hatches for the full story.
A parser quirk (tracked, fix planned)

Writing the body as a single expression, strsplit(x, pat)[[1]], confuses the parser in the current release. Indexing a call result directly is rejected only in tail position of a fn body; the same code parses fine anywhere else. Two workarounds: bind the result to a variable first, then index it (as shown above), or parenthesize the call, (strsplit(x, pat))[[1]].

The bug is cataloged as an open, automatically replayed case in the compiler's bug catalog (typr case, case 0107-call-result-index-in-fn-tail): every compiler run of the catalog re-checks that this snippet still fails, and the case flips to fixed once the parser accepts it. Remove this note when that happens.

typr check: your new best friend​

Before building anything, ask the compiler what it thinks:

typr check
Parsing... done
Type checking... done
Code verification successful!

Now for the whole point of TypR. Add a bad call at the bottom of TypR/main.ty, passing a number where a string is expected:

This example does not compile It shows what TypR rejects, on purpose.
# --- setup: the function under test, from the previous block ---
@pub let str_split_one <- fn(x: char, pat: char): [char] {
  let parts <- strsplit(x, pat);
  parts[[1]] as! [char]
};
# ----------------------------------------------------------------

let r <- str_split_one(42, ",");
typr check
Type errors found:
  × Type error: No signature of function 'str_split_one' matches this call.
   ╭─[TypR/main.ty:6:10]
 6 │ let r <- str_split_one(42, ",");
   ·          ──────┬──────
   ·                ╰── Called with 2 argument(s): (42, ",")
   ╰────
  help: 'str_split_one' exists but none of its signature(s) accepts these
        arguments:
            (char, char) -> [any, char]

In pure R this bug would only surface at run time, possibly hours into a pipeline. Here it shows up before the code ever runs. Delete the bad line and commit:

git add TypR/main.ty
git commit -m "Add str_split_one()"

typr build​

Transpile the typed source into R:

typr build
R code successfully generated in the R/ folder

What appeared:

R/
  std.R                # generated helpers
  generic_functions.R  # generated generics
  types.R              # generated type definitions
  main.R               # transpiled main.ty
man/
  str_split_one.Rd     # documentation, from the annotations

Two results are worth pausing on:

  1. R/main.R contains ordinary R code with roxygen2 comments derived from the types. No #' is ever written by hand.
  2. man/str_split_one.Rd was generated automatically. str_split_one is already a documented, exported function:
\name{str_split_one}
\title{Split a single string into pieces.}
\usage{str_split_one(x, pat)}

Test { } and typr test​

In the R world this is where usethis::use_testthat() and a test file come in. In TypR, tests live next to the code in a Test block and are extracted into tests/testthat/ at build time. Add this to TypR/main.ty:

# --- setup: the function under test ---
@pub let str_split_one <- fn(x: char, pat: char): [char] {
  let parts <- strsplit(x, pat);
  parts[[1]] as! [char]
};
# --------------------------------------

Test {
  test_that("str_split_one splits a string", {
    expect_equal(str_split_one("a,b,c", ","), c("a", "b", "c"));
  });

  test_that("str_split_one returns one piece per separator group", {
    expect_equal(length(str_split_one("a,b", ",")), 2);
  })
}

Rebuild and run the test suite:

typr build   # extracts the block to tests/testthat/test-main.R
typr test    # runs devtools::test()
Supported testthat assertions

The current release only types a subset of testthat inside Test blocks: expect_equal and expect_true are available, while expect_error, expect_length, and expect_type are rejected with not defined in this scope. The natural test for this section would have been expect_error(str_split_one("a,b,c", "")); it is replaced above by an equivalent assertion on the result. Support for the full testthat assertion surface is planned for a future release.

A renv quirk (fixed in the next release)

In the current release, typr test can fail inside a freshly scaffolded project because the renv environment does not see the globally installed devtools and testthat packages, even after typr init. As a workaround, run the tests from a plain R session at the project root with devtools::test(). This environment friction is exactly what the planned move to Rig will address in the next release.

Commit the tests:

git add TypR/main.ty tests/testthat/
git commit -m "Add tests for str_split_one()"

typr pkg install: use the package​

The result is a perfectly ordinary R package. Install it into the R library:

typr pkg install

(devtools::install() from R works too.) Then, in a fresh R session:

library(regexcite)

str_split_one("alfa,bravo,charlie,delta", ",")
#> [1] "alfa"    "bravo"   "charlie" "delta"

?str_split_one   # yes, the help page is already there

Final commit:

git add .
git commit -m "Build, document and test regexcite"

Review: what just happened?​

R Packages (2e) stepTypR equivalent
usethis::create_package()typr new regexcite
usethis::use_git()git init
Write R/str-split-one.R by handWrite TypR/main.ty, run typr build
devtools::document()automatic from @pub + type annotations
usethis::use_testthat() + test filesTest { } blocks, extracted at build
devtools::test()typr test
devtools::install()typr pkg install
R CMD checktypr check, before anything runs

The whole game, in one sentence: typed code goes in TypR/, and the compiler turns it into a conventional, documented, tested, installable R package while catching type errors before R ever sees them.

Where to go next​