Unknown values
R has constructs no static checker can follow, such as data frame columns and S4 and R6 objects.
Their values are Unknown, which is compatible with everything. ry would rather skip a check than
report something false, so one unmodeled construct never causes a cascade of errors.
The cost is that a clean run does not say how much was checked.
Strict mode reports the places a value became Unknown.
Giving a value a type
Section titled “Giving a value a type”An annotation gives an Unknown value a type, so every later use is checked against it:
#: doubleamount <- df$amountamount: double <- df$amountOverriding inference
Section titled “Overriding inference”A plain annotation is checked against the value. Two forms give a type without that check, and
inline they go between a binding’s : and its type, as in amount: @if-unknown double <- df$amount.
#: @if-unknown TYPE types an Unknown value, such as a data frame column, and becomes an error
once ry can infer the type, so it never goes stale:
#: @if-unknown doubleamount <- df$amount
#: @if-unknown doublen <- 1.5type-mismatch
x `@if-unknown` applies only where the type is unknown, and this is already `double`. Drop the annotation, or use `#:` to check the type or `@trust` to override itA value declared Any, such as the result of readRDS(), takes a plain annotation instead.
#: @trust TYPE overrides a type ry did infer, for the cases where you know better.
Any is also compatible with everything, but it is a deliberate choice rather than a gap. A value
declared Any, file reads such as readRDS(), eval(), and calls to S3 generics that dispatch
through UseMethod() are Any, and strict mode does not report them.
Data frames
Section titled “Data frames”data.frame() returns a data.frame, but a base data frame’s columns are not typed, so
df$amount is Unknown. An annotation such as df: data.frame checks that a data frame arrives,
but not which columns it has. ry’s built-in DataFrame is the typed
alternative, with every column, query and join checked. For analysis code this is the gap that
matters most, and a column read such as df$amount is usually the first place strict mode reports.
Package stubs
Section titled “Package stubs”The checker never runs R, so it cannot ask a package what it exports. It reads stubs instead: declaration
files that list a package’s names and their types, the way TypeScript reads .d.ts files.
What ships
Section titled “What ships”Base R and its default packages are typed, and so are data.table, dplyr, ggplot2, and
testthat. For the rest of R’s own packages, the tidyverse, and knitr, rlang, glue,
magrittr, scales, jsonlite, and R6, ry ships export lists: the names without their types,
which is enough to keep unresolved-name checks working.
Bare names from base R’s default packages always resolve. Every other package counts once your
project uses it, through a library() call, a DESCRIPTION dependency, or a NAMESPACE import.
Until then mutate is unresolved, as it would be in R.
A package ry does not know
Section titled “A package ry does not know”Attaching a package with no stub means any bare name in the project might be one of its exports. ry cannot tell a typo from an export it has never heard of, so it stops reporting unresolved names across the whole project:
library(dbclient)reslt <- 1print(result) # not reportedA stub turns the check back on. Put it under stubs/, named after the package:
@type Sessionconnect : fn(host: character) -> SessionNow the typo is reported, and with type checking on, so are calls that do not fit the declarations:
library(dbclient)session <- connect(42) # expected `character`, found `double`print(result) # I could not resolve `result` in this package, its imports, or builtins.dbclient::conect("db") # `conect` is not exported by `dbclient`.Declare only what you call: a name you leave out is reported wherever you use it, which tells you what to add next. Even an empty stub file turns the unresolved-name check back on.
Overriding a shipped stub
Section titled “Overriding a shipped stub”A declaration under stubs/ replaces a shipped one with the same name, so you can fix a wrong
return type, or add a function your version of the package has, without waiting for a release.
ry check reports every line it could not load. Authoring stubs has
the full format, including overloads, data-masking functions, and @pure for functions that
strict mode lets a package constant call.
Limitations
Section titled “Limitations”Most gaps are silent, because a value ry cannot type becomes Unknown and its checks are skipped.
Strict mode shows most of the places they occur.
Not checked
Section titled “Not checked”- Data frame columns. Everything read out of a base data frame is
Unknown, sodf$whateverpasses even if the column does not exist. ADataFrametypes its columns. - Matrix dimensions. Matrix arithmetic is checked, but a non-conformable product is not.
Indexing with several indices, as in
m[i, j], or withdrop = FALSEgivesUnknown. - S4 and R6 objects are
Unknown, and calls to an S3 generic that dispatches throughUseMethod()returnAny. Operator methods such as+.Dateand methods called directly are checked. Custom types shows how to check who receives an object. - Packages without a stub. Their names are
Unknown, and attaching one switches off unresolved-name reports for the whole project. $on an atomic vector in a package that defines a$method. A classed vector may carry that method, so the read isUnknowninstead of an error.- Some paths a test elsewhere in the body rules out. A list member beside a number in
arithmetic, a list or
NULLbeside an operand of unknown type, theNULLof a computed list field used as a:endpoint, and the logical member in the false branch ofisTRUE()orisFALSE()are not reported, because correct R usually guards them in ways ry cannot follow. - A placeholder local. A variable whose only visible value is
NULLor a bareNA, filled later byassign()or<<-, is not checked where it is passed as an argument or tested as a condition. - A project’s own
%op%operators, because one may quote its right operand instead of evaluating it, as magrittr’s%>%does.
Where correct code is reported
Section titled “Where correct code is reported”These are known bugs, not design decisions:
- A bare
NAunder a declared return type. A function declared-> characterthat returnsNAon one path is reported, becauseNAislogical. ReturnNA_character_instead. - A trailing comma for rlang’s dynamic dots. dplyr’s verbs accept
mutate(d, y = 1, ), but ry reports the trailing comma as it would forc(1, 2, ). - Package-qualified calls to a package ry does not know.
pkg::fun()reports an unknown namespace when ry has no stub or export list forpkg, even if a file callslibrary(pkg). - A generic function passed inline as an argument.
lapply(xs, function<T>(v: T) -> T v)is reported, because type parameters on a function that is not bound to a name are not instantiated. Leave the function’s types off, or bind it to a name first.