Vectors, lists and NULL
Three of R’s most common runtime errors come from shapes: a vector of length two where an if
expects one value, a misspelled list field that silently reads as NULL, and a NULL that nobody
handled. ry’s types record all three, so it reports them before the code runs, while your code stays
the same R.
Scalars and vectors
Section titled “Scalars and vectors”R has no scalars: 1L is an integer vector of length one. But an if condition, an operand of
&&, and an endpoint of : need exactly one element, and passing a longer vector there is a bug R
reports late or not at all. So ry tracks the length-one case as its own type:
n <- 1L # integer: exactly one elementids <- c(1L, 2L) # integer[]: any lengthages <- c(ada = 36L, bo = 4L) # integer[named]: any length, with namesA scalar is accepted where a vector is expected, but not the reverse, because a vector of unknown length may not have exactly one element:
#: fn(flag: logical) -> characterlabel <- function(flag) if (flag) "yes" else "no"
label(c(TRUE, FALSE))label <- function(flag: logical) -> character if (flag) "yes" else "no"
label(c(TRUE, FALSE))type-mismatch
x expected `logical`, found `logical[]`A double accepts an integer or a logical, which R converts without loss. A number is never
accepted where character is expected: R would convert it, but a silent number-to-string conversion
is usually a bug, so ry asks for an explicit as.character().
Four kinds of list
Section titled “Four kinds of list”R uses one list type for four different jobs, and each job has its own mistakes, so ry gives each
its own type. Braces mean a fixed shape (list{...}), and brackets mean any length (list[...]). At run time all four are the same R list, so your code does not change.
pair <- list(1L, "ok") # positional: list{integer, character}person <- list(name = "Ada", age = 36L) # named fields: list{name: character, age: integer}sizes <- lapply(words, nchar) # any length: list[integer]
#: list[named: integer]counts <- list(apples = 3L) # by key: list[named: integer]pair <- list(1L, "ok") # positional: list{integer, character}person <- list(name = "Ada", age = 36L) # named fields: list{name: character, age: integer}sizes <- lapply(words, nchar) # any length: list[integer]
counts: list[named: integer] <- list(apples = 3L) # by keyFixed-shape lists
Section titled “Fixed-shape lists”A list of positional values or named fields has a fixed shape, so ry knows each position and field and its type.
That makes $ checkable, which matters because R answers a misspelled field with a silent NULL
that fails somewhere else:
person$nmaetype-mismatch
x field `nmae` does not exist in `list{name: character, age: integer}`. Did you mean `name`? --[R/person.R:2:8] 2 | person$nmae | ^^^^A fixed shape is also exact, so a list with an extra field where a fixed shape is expected is an error, because an unexpected field is usually a misspelled one.
Lists of any length
Section titled “Lists of any length”A list of any length, or one looked up by key, can have any length, so ry knows only its element type. Reading a key
gives the element type or NULL, because the key may be missing:
counts[["pears"]] # integer | NULLA list(...) literal infers as a fixed shape, so a dictionary that starts from a literal needs an
annotation, as counts does. An empty list() filled by computed keys, as in d[[key]] <- value,
needs none.
Many R functions return NULL for “nothing”, and code that forgets the case fails far from where the
NULL came from. A value that may be NULL has a union type, such as character | NULL.
The most common case is an optional argument that defaults to NULL. Its type says what a caller
may pass instead. In the fn(...) form, brackets around a name such as [label] mark an optional
argument:
#: fn(id: integer, [label]: character | NULL) -> characterlabel_for <- function(id, label = NULL) { if (is.null(label)) paste("item", id) else label}label_for(1L, label = 2)label_for <- function(id: integer, label: character | NULL = NULL) -> character { if (is.null(label)) paste("item", id) else label}label_for(1L, label = 2)type-mismatch
x expected `character | NULL`, found `double`Reading through a value that may be NULL
Section titled “Reading through a value that may be NULL”A field of a value that may be NULL may be NULL too, because R reads every field of NULL as
NULL. Returning such a field where an integer is declared is an error:
#: fn(config: list{retries: integer} | NULL) -> integerretries <- function(config) { config$retries}retries <- function(config: list{retries: integer} | NULL) -> integer { config$retries}type-mismatch
x expected `integer`, found `integer | NULL`Check for NULL first, and the error goes away:
#: fn(config: list{retries: integer} | NULL) -> integerretries <- function(config) { if (is.null(config)) return(3L) config$retries}retries <- function(config: list{retries: integer} | NULL) -> integer { if (is.null(config)) return(3L) config$retries}The guard narrows config to the list for the rest of the function. is.character(),
is.numeric(), is.list(), and the other is.* tests narrow the same way.
Narrowing applies to variables only, so to test a field, copy it into a variable first:
r <- config$retries; if (is.null(r)) ....
ry checks the NULL you declare. A variable that the body computes without an annotation, such as
the result of a switch without a default, is passed on as its value without the NULL, because the
code presumably never reaches the call on the NULL path. The same holds for the NULL of an
unannotated parameter, from a NULL default or an is.null() test. To have such a value checked, annotate the assignment or the function.