Skip to content

feddelegrand7/sicher

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

44 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

sicher

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. πŸ›‘οΈ

πŸ“¦ Installation

Install the development version from GitHub:

# install.packages("devtools")
devtools::install_github("feddelegrand7/sicher")

πŸš€ Quick Start

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: thirty

✨ Core Features

🧩 Built-in Types

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

βš™οΈ Operators

Operator Purpose
%:% Annotate a variable with a type
%<-% Assign a value (type-checked)

πŸ”§ Type Modifiers

1️⃣ Scalar() β€” single-element values

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]

πŸ”’ Readonly() β€” immutable variables

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.

❓ Optional() β€” nullable values

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: 123

πŸ”€ Union Types

Accept 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: TRUE

πŸ“ Size-constrained Vectors

Append [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]

πŸ“‹ Structured List Types

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]

πŸ—„οΈ Data Frame Schemas

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]

πŸ“¦ Homogeneous Lists with ListOf()

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 2

πŸ› οΈ Custom Types

Use 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: -1

πŸ”€ Typed Functions

Use 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: a
Num_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: TRUE

You 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

🌍 Function usage

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

πŸ” Type inference

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

πŸ“š Learn More

Full documentation and worked examples are available at the package website.

About

Runtime type safety from R for R πŸ”

Resources

License

Unknown, MIT licenses found

Licenses found

Unknown
LICENSE
MIT
LICENSE.md

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors