library(sicher)
# Basic types
x %:% Integer %<-% 42L
y %:% Double %<-% 3.14
z %:% Numeric %<-% 100
name %:% String %<-% "Alice"
flag %:% Bool %<-% TRUE
items %:% List %<-% list(1, 2, 3)
anything %:% Any %<-% "could be anything"
nothing %:% Null %<-% NULL
func %:% Function %<-% function(x) x + 1
df %:% DataFrame %<-% data.frame(a = 1:3, b = letters[1:3])Features
Core Features
Built-in Types
sicher provides a comprehensive set of built-in types:
Type Modifiers
Scalar Types
Enforce single values (vectors of length 1):
# This works
single_value %:% Scalar(Numeric) %<-% 42
# This fails
single_value <- c(1, 2, 3)Error: Type error in 'single_value': Expected scalar<numeric>, got double of length 3
Received: [1, 2, 3]
Readonly Types
Prevent reassignment after initial value:
# Set a readonly constant
PI %:% Readonly(Double) %<-% 3.14159
PI <- 3.0 # This failsError: Cannot reassign readonly variable 'PI'. Remove Readonly() from the type declaration if mutation is needed.
Optional Types
Allow NULL values in addition to the base type:
PI <- 3.0Error: Cannot reassign readonly variable 'PI'. Remove Readonly() from the type declaration if mutation is needed.
# Optional string (can be string or NULL)
middle_name %:% Optional(String) %<-% NULL
middle_name <- "Marie" # Also OK
middle_name <- 123 # This failsError: Type error in 'middle_name': Expected string | null, got double
Received: 123
Vector Size Types
Specify exact vector lengths:
# Exactly 3 numeric values
coords %:% Numeric[3] %<-% c(1, 2, 3)
coords <- c(4, 5, 6) # This works
coords <- c(1, 2) # Too shortError: Type error in 'coords': Expected numeric[3], got double of length 2
Received: [1, 2]
Union Types
Allow multiple types using the | operator:
# Accept either string or numeric
id %:% (String | Numeric) %<-% "user123"
id <- 456 # Also OK
id <- TRUEError: Type error in 'id': Expected string | numeric, got bool
Received: TRUE
Structured List Types
Define object-like structures for lists:
# Define a Person type
Person <- create_list_type(list(
name = String,
age = Numeric,
email = Optional(String)
))
# Use it
person %:% Person %<-% list(
name = "Alice",
age = 30,
email = "alice@example.com"
)
# Missing required field fails
person <- list(name = "Bob")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]
# Extra fields not allowed fail
person <- list(
name = "Charlie",
age = 25,
email = "charlie@example.com",
extra = "not allowed"
)Error: Type error: Expected {name: string, age: numeric, email?: string | null}, got list
Details: Unexpected field: 'extra' (valid fields: name, age, email)
Received: list with fields: [name, age, email, extra]
Data Frame Types
Define schemas for data frames:
# Define a user table schema
UserTable <- create_dataframe_type(list(
id = Integer,
username = String,
active = Bool,
last_login = Optional(String)
))
# Valid data frame
users %:% UserTable %<-% data.frame(
id = 1:3,
username = c("alice", "bob", "charlie"),
active = c(TRUE, FALSE, TRUE)
)
# Missing required column fails
users <- data.frame(
username = c("alice", "bob"),
active = c(TRUE, FALSE)
)Error: Type error: Expected data.frame{id: integer, username: string, active: bool, last_login?: string | null}, got data.frame[2 x 2]
Details: Missing required column(s): id (expected columns: id, username, active)
users <- data.frame(
id = 1:2,
username = c("alice", "bob"),
active = c("yes", "no") # Should be logical
)Error: Type error in 'active': Expected bool, got string of length 2
Received: [yes, no]
Homogeneous List Types
For lists of similar items (like API responses):
# Define item structure
TodoItem <- create_list_type(list(
userId = Numeric,
id = Numeric,
title = String,
completed = Bool
))
# List of todo items
TodoList <- ListOf(TodoItem)
# Valid list
todos %:% TodoList %<-% list(
list(userId = 1, id = 1, title = "Task 1", completed = FALSE),
list(userId = 1, id = 2, title = "Task 2", completed = TRUE)
)
todos <- list(
list(userId = 1, id = 1, title = "Task 1", completed = FALSE),
list(wrong = "structure")
)Error: Type error in 'todos': Expected list<{userId: numeric, id: numeric, title: string, completed: bool}>, got list of length 2
Received: list of length 2
Combining Features
All features can be combined:
# Complex nested structure
Company <- create_list_type(list(
name = String,
employees = ListOf(create_list_type(list(
name = String,
salary = Numeric,
department = Optional(String)
))),
founded = Numeric[3] # [year, month, day]
))
company %:% Company %<-% list(
name = "Tech Corp",
employees = list(
list(name = "Alice", salary = 75000, department = "Engineering"),
list(name = "Bob", salary = 65000) # department optional
),
founded = c(2020, 1, 15)
)Advanced Usage
Typed Functions
Use typed_function() to wrap any function with runtime type checks on its parameters and, optionally, its return value:
# Basic typed function with parameter and return type checking
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: a
# Optional parameter — title may be a String or NULL
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 — id can be String or Numeric
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: TRUE
Custom Types
Create your own types using create_type():
# Positive number type
Positive <- create_type("positive", function(x) {
is.numeric(x) && all(x > 0)
})
value %:% Positive %<-% 5
value <- -1Error: Type error in 'value': Expected positive, got double
Received: -1
Type Composition
Build complex types from simpler ones:
# Email type
Email <- create_type("email", function(x) {
is.character(x) && length(x) == 1 &&
grepl("^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$", x)
})
# Person with email
PersonWithEmail <- create_list_type(list(
name = String,
email = Email,
age = Numeric
))
person %:% PersonWithEmail %<-% list(
name = "Alice",
email = "alice@example.com",
age = 30
)
person %:% PersonWithEmail %<-% list(
name = "Bob",
email = "invalid-email",
age = 25
)Error: Type error in 'email': Expected email, got string
Received: invalid-email
Error Messages
sicher provides clear, contextual error messages:
# Type mismatch
x %:% Numeric %<-% "not a number"Error: Type error in 'x': Expected numeric, got string
Received: not a number
# Missing required field
PersonType <- create_list_type(list(name = String, age = Numeric))
p %:% PersonType %<-% list(name = "Alice")Error: Type error: Expected {name: string, age: numeric}, got list
Details: Missing required field(s): age (expected fields: name, age)
Received: list with fields: [name]
# Wrong vector length
vec %:% Numeric[3] %<-% c(1, 2)Error: Type error in 'vec': Expected numeric[3], got double of length 2
Received: [1, 2]