sicher (German for safe or certain) is an R package that brings runtime type safety to R programming β inspired by TypeScript for JavaScript. Declare types for your variables and have them enforced automatically on every assignment, catching type errors early and making your code more robust and self-documenting. π‘οΈ
Install the development version from GitHub:
# install.packages("devtools")
devtools::install_github("feddelegrand7/sicher")library(sicher)
# Annotate a variable with a type, then assign a value
name %:% String %<-% "Alice"
age %:% Numeric %<-% 30
# The type is enforced on every subsequent assignment
age <- "thirty" # Error: Type error
#> Error: Type error in 'age': Expected numeric, got string
#> Received: thirtysicher ships with a complete set of primitive and container types:
| Type | Checks |
|---|---|
Integer |
is.integer() |
Double |
is.double() |
Numeric |
is.numeric() |
String |
is.character() |
Bool |
is.logical() |
List |
is.list() |
DataFrame |
is.data.frame() |
Function |
is.function() |
Any |
always passes |
Null |
is.null() |
x %:% Integer %<-% 42L
y %:% Double %<-% 3.14
flag %:% Bool %<-% TRUE
df %:% DataFrame %<-% data.frame(a = 1:3)| Operator | Purpose |
|---|---|
%:% |
Annotate a variable with a type |
%<-% |
Assign a value (type-checked) |
single %:% Scalar(Numeric) %<-% 42
single <- c(1, 2, 3) # Error: length > 1
#> Error: Type error in 'single': Expected scalar<numeric>, got double of length 3
#> Received: [1, 2, 3]PI %:% Readonly(Double) %<-% 3.14159
PI <- 3.0 # Error: cannot reassign readonly variable
#> Error: Cannot reassign readonly variable 'PI'. Remove Readonly() from the type declaration if mutation is needed.middle_name %:% Optional(String) %<-% NULL # OK
middle_name <- "Marie" # Also OK
middle_name <- 123 # Error: not string or null
#> Error: Type error in 'middle_name': Expected string | null, got double
#> Received: 123Accept more than one type with |:
id %:% (String | Numeric) %<-% "user123"
id <- 456 # Also OK
id <- TRUE # Error: not string or numeric
#> Error: Type error in 'id': Expected string | numeric, got bool
#> Received: TRUEAppend [n] to any type to require an exact vector length:
coords %:% Numeric[3] %<-% c(1, 2, 3)
coords <- c(4, 5, 6) # OK β same length
coords <- c(1, 2) # Error: wrong length
#> Error: Type error in 'coords': Expected numeric[3], got double of length 2
#> Received: [1, 2]Define object-like schemas with create_list_type():
Person <- create_list_type(list(
name = String,
age = Numeric,
email = Optional(String) # nullable field
))
person %:% Person %<-% list(name = "Alice", age = 30, email = "alice@example.com")
person <- list(name = "Bob") # Error: missing required field 'age'
#> Error: Type error: Expected {name: string, age: numeric, email?: string | null}, got list
#> Details: Missing required field(s): age (expected fields: name, age)
#> Received: list with fields: [name]Validate column names and types with create_dataframe_type():
UserTable <- create_dataframe_type(list(
id = Integer,
username = String,
active = Bool
))
users %:% UserTable %<-% data.frame(
id = 1:2,
username = c("alice", "bob"),
active = c(TRUE, FALSE)
)
# Wrong column type fails immediately
users <- data.frame(
id = c("1", "2"), # Error: id must be integer
username = c("alice", "bob"),
active = c(TRUE, FALSE)
)
#> Error: Type error in 'id': Expected integer, got string of length 2
#> Received: [1, 2]Validate every element of a list against the same type:
TodoItem <- create_list_type(list(
id = Numeric,
title = String,
completed = Bool
))
TodoList <- ListOf(TodoItem)
todos %:% TodoList %<-% list(
list(id = 1, title = "Buy milk", completed = FALSE),
list(id = 2, title = "Read book", completed = TRUE)
)
todos <- list(
list(id = 1, title = "Buy milk", completed = FALSE),
list(wrong = "shape") # Error: element does not match TodoItem
)
#> Error: Type error in 'todos': Expected list<{id: numeric, title: string, completed: bool}>, got list of length 2
#> Received: list of length 2Use create_type() to define your own validator with any predicate
function:
Positive <- create_type("positive", function(x) is.numeric(x) && all(x > 0))
value %:% Positive %<-% 5
value <- -1 # Error
#> Error: Type error in 'value': Expected positive, got double
#> Received: -1Use typed_function() to wrap any function with runtime type checks on
its parameters and, optionally, its return value β a typed function
signature for R:
# Basic typed function β checks params and return type
add <- typed_function(
function(x, y) x + y,
params = list(x = Numeric, y = Numeric),
.return = Numeric
)
add(1, 2) # Returns 3
#> [1] 3
add("a", 2) # Error: Type error in 'x'
#> Error: Type error in 'x': Expected numeric, got string
#> Received: aNum_not_inf <- create_type(
name = "Num_not_inf",
checker = function(x) {
is.numeric(x) && !is.infinite(x)
}
)
divide <- function(a, b) {
return(a / b)
}
divide_safe <- typed_function(
fn = divide,
params = list(
a = Num_not_inf,
b = Num_not_inf
),
.return = Num_not_inf
)
divide_safe(10, 2) # works normally
#> [1] 5
divide_safe(10, 0) # fails as 10/0 returns Inf
#> Error: Type error in '<return value>': Expected Num_not_inf, got double
#> Received: Inf# Optional parameter
greet <- typed_function(
function(name, title = NULL) {
if (is.null(title)) paste("Hello,", name)
else paste("Hello,", title, name)
},
params = list(name = String, title = Optional(String))
)
greet("Alice") # "Hello, Alice"
#> [1] "Hello, Alice"
greet("Alice", title = "Dr.") # "Hello, Dr. Alice"
#> [1] "Hello, Dr. Alice"
greet("Alice", title = 42) # Error: Type error in 'title'
#> Error: Type error in 'title': Expected string | null, got double
#> Received: 42# Union type in params
describe <- typed_function(
function(id) paste("ID:", id),
params = list(id = String | Numeric),
.return = String
)
describe("abc") # "ID: abc"
#> [1] "ID: abc"
describe(123) # "ID: 123"
#> [1] "ID: 123"
describe(TRUE) # Error: Type error in 'id'
#> Error: Type error in 'id': Expected string | numeric, got bool
#> Received: TRUEYou can also define an object to expect as a return value:
Person <- create_list_type(
type_spec = list(
name = String,
age = Numeric
)
)
get_person_info_as_list <- function(name, age) {
return(list(
name = name,
age = age
))
}
get_person_info_as_message <- function(name, age) {
return(
paste("Hi my name is ", name, " I'm ", age, " years old")
)
}
get_person_info_as_list_safe <- typed_function(
fn = get_person_info_as_list,
params = list(name = String, age = Numeric),
.return = Person
)
get_person_info_as_list_safe(name = "Omar", age = 30) # works fine
#> $name
#> [1] "Omar"
#>
#> $age
#> [1] 30
get_person_info_as_message_safe <- typed_function(
fn = get_person_info_as_message,
params = list(name = String, age = Numeric),
.return = Person
)
# Should fail as the function does not return a Person list anymore
get_person_info_as_message_safe(name = "Omar", age = 30)
#> Error: Type error in '<return value>': Expected {name: string, age: numeric}, got string
#> Received: Hi my name is Omar I'm 30 years old# Catch bad payroll data early instead of getting silent NAs
calculate_mean_payroll <- function(salaries) {
salaries %:% Numeric %<-% salaries
mean(salaries)
}
calculate_mean_payroll(c(1800, 2300, 4000)) # Works fine
#> [1] 2700
calculate_mean_payroll(c(1800, "2300", 4000)) # Error: type mismatch
#> Error: Type error in 'salaries': Expected numeric, got string of length 3
#> Received: [1800, 2300, 4000]You can automatically infer the type for an R object using
infer_type():
# Primitives
infer_type(42L) # Integer
#> <type: integer >
infer_type(3.14) # Double
#> <type: double >
infer_type("abc") # String
#> <type: string >
infer_type(TRUE) # Bool
#> <type: bool >
infer_type(NULL) # Null
#> <type: null >
infer_type(function(x) x + 1) # Function
#> <type: Function >
# Default mode infers types, not observed lengths
infer_type(c(1L, 2L, 3L)) # Integer
#> <type: integer >
infer_type(c(1, 2, 3)) # Double
#> <type: double >
infer_type(c("a", "b")) # String
#> <type: string >
infer_type(c(TRUE, FALSE)) # Bool
#> <type: bool >
# Named and unnamed lists
infer_type(list(a = 1L, b = "x")) # create_list_type(list(a = Integer, b = String))
#> <type: {a: integer, b: string} >
infer_type(list(1L, 2L, 3L)) # ListOf(Integer)
#> <type: list<integer> >
infer_type(list(1L, "a")) # List
#> <type: list >
infer_type(list(a = NULL, b = 1)) # create_list_type(list(a = Optional(Any), b = Double))
#> <type: {a?: any | null, b: double} >
# Data frames
infer_type(data.frame(x = 1:3, y = c("a", "b", "c"), stringsAsFactors = FALSE))
#> <type: data.frame{x: integer, y: string} >
# create_dataframe_type(list(x = Integer, y = String))
# Use strict = TRUE to also infer Scalar() and [n] size constraints
infer_type(42L, strict = TRUE) # Scalar(Integer)
#> <type: scalar<integer> >
infer_type(c("a", "b"), strict = TRUE) # String[2]
#> <type: string[2] >
infer_type(data.frame(x = 1:3), strict = TRUE)
#> <type: data.frame{x: integer[3]} >Full documentation and worked examples are available at the package website.
