Features

Core Features

Built-in Types

sicher provides a comprehensive set of built-in types:

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])

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 fails
Error: 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.0
Error: 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 fails
Error: 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 short
Error: 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 <- TRUE
Error: 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 <- -1
Error: 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]