The interface
package provides a system for defining and
implementing interfaces in R, with runtime type checking. This approach
brings some of the benefits of statically-typed languages to R, allowing
for more structured and safer code.
Interfaces in R can be beneficial for several reasons:
interface
package provides the following:
To install the package, use the following command:
# Install the package from the source
::install_github("dereckmezquita/interface") remotes
Import the package functions.
::use(interface[interface, type.frame, fun, enum]) box
To define an interface, use the interface()
function:
# Define an interface
<- interface(
Person name = character,
age = numeric,
email = character
)
# Implement the interface
<- Person(
john name = "John Doe",
age = 30,
email = "john@example.com"
)
print(john)
#> Object implementing interface:
#> name: John Doe
#> age: 30
#> email: john@example.com
#> Validation on access: Disabled
# interfaces are lists
print(john$name)
#> [1] "John Doe"
# Valid assignment
$age <- 10
john
print(john$age)
#> [1] 10
# Invalid assignment (throws error)
try(john$age <- "thirty")
#> Error : Property 'age' must be of type numeric
You can create nested interfaces and extend existing interfaces:
# Define an Address interface
<- interface(
Address street = character,
city = character,
postal_code = character
)
# Define a Scholarship interface
<- interface(
Scholarship amount = numeric,
status = logical
)
# Extend the Person and Address interfaces
<- interface(
Student extends = c(Address, Person), # will inherit properties from Address and Person
student_id = character,
scores = data.table::data.table,
scholarship = Scholarship # nested interface
)
# Implement the extended interface
<- Student(
john_student name = "John Doe",
age = 30,
email = "john@example.com",
street = "123 Main St",
city = "Small town",
postal_code = "12345",
student_id = "123456",
scores = data.table::data.table(
subject = c("Math", "Science"),
score = c(95, 88)
),scholarship = Scholarship(
amount = 5000,
status = TRUE
)
)
print(john_student)
#> Object implementing interface:
#> student_id: 123456
#> scores: Math
#> scores: Science
#> scores: 95
#> scores: 88
#> scholarship: <environment: 0x120ae8700>
#> street: 123 Main St
#> city: Small town
#> postal_code: 12345
#> name: John Doe
#> age: 30
#> email: john@example.com
#> Validation on access: Disabled
Interfaces can have custom validation functions:
<- function(x) {
is_valid_email grepl("[a-z|0-9]+\\@[a-z|0-9]+\\.[a-z|0-9]+", x)
}
<- interface(
UserProfile username = character,
email = is_valid_email,
age = function(x) is.numeric(x) && x >= 18
)
# Implement with valid data
<- UserProfile(
valid_user username = "john_doe",
email = "john@example.com",
age = 25
)
print(valid_user)
#> Object implementing interface:
#> username: john_doe
#> email: john@example.com
#> age: 25
#> Validation on access: Disabled
# Invalid implementation (throws error)
try(UserProfile(
username = "jane_doe",
email = "not_an_email",
age = "30"
))#> Error : Errors occurred during interface creation:
#> - Invalid value for property 'email': FALSE
#> - Invalid value for property 'age': FALSE
Enums provide a way to define a set of named constants. They are useful for representing a fixed set of values and can be used in interfaces to ensure that a property only takes on one of a predefined set of values.
# Define an enum for days of the week
<- enum("Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday")
DaysOfWeek
# Create an enum object
<- DaysOfWeek("Wednesday")
today print(today)
#> Enum: Wednesday
# Valid assignment
$value <- "Friday"
todayprint(today)
#> Enum: Friday
# Invalid assignment (throws error)
try(today$value <- "NotADay")
#> Error in `$<-.enum`(`*tmp*`, value, value = "NotADay") :
#> Invalid value. Must be one of: Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday
Enums can be used as property types in interfaces:
# Define an interface using an enum
<- interface(
Meeting title = character,
day = DaysOfWeek,
start_time = numeric,
duration = numeric
)
# Create a valid meeting object
<- Meeting(
standup title = "Daily Standup",
day = "Monday",
start_time = 9.5, # 9:30 AM
duration = 0.5 # 30 minutes
)
print(standup)
#> Object implementing interface:
#> title: Daily Standup
#> day: Monday
#> start_time: 9.5
#> duration: 0.5
#> Validation on access: Disabled
# Invalid day (throws error)
try(Meeting(
title = "Invalid Meeting",
day = "InvalidDay",
start_time = 10,
duration = 1
))#> Error : Errors occurred during interface creation:
#> - Invalid enum value for property 'day': Invalid value. Must be one of: Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday
You can also declare enums directly within an interface definition:
# Define an interface with an in-place enum
<- interface(
Task description = character,
priority = enum("Low", "Medium", "High"),
status = enum("Todo", "InProgress", "Done")
)
# Create a task object
<- Task(
my_task description = "Complete project report",
priority = "High",
status = "InProgress"
)
print(my_task)
#> Object implementing interface:
#> description: Complete project report
#> priority: High
#> status: InProgress
#> Validation on access: Disabled
# Update task status
$status$value <- "Done"
my_taskprint(my_task)
#> Object implementing interface:
#> description: Complete project report
#> priority: High
#> status: Done
#> Validation on access: Disabled
# Invalid priority (throws error)
try(my_task$priority$value <- "VeryHigh")
#> Error in `$<-.enum`(`*tmp*`, value, value = "VeryHigh") :
#> Invalid value. Must be one of: Low, Medium, High
Enums provide an additional layer of type safety and clarity to your code. They ensure that certain properties can only take on a predefined set of values, reducing the chance of errors and making the code more self-documenting.
Define functions with strict type constraints:
<- fun(
typed_fun x = numeric,
y = numeric,
return = numeric,
impl = function(x, y) {
return(x + y)
}
)
# Valid call
print(typed_fun(1, 2)) # [1] 3
#> [1] 3
# Invalid call (throws error)
try(typed_fun("a", 2))
#> Error : Property 'x' must be of type numeric
<- fun(
typed_fun2 x = c(numeric, character),
y = numeric,
return = c(numeric, character),
impl = function(x, y) {
if (is.numeric(x)) {
return(x + y)
else {
} return(paste(x, y))
}
}
)
print(typed_fun2(1, 2)) # [1] 3
#> [1] 3
print(typed_fun2("a", 2)) # [1] "a 2"
#> [1] "a 2"
data.frame
/data.table
sCreate data frames with column type constraints and row validation:
<- type.frame(
PersonFrame frame = data.frame,
col_types = list(
id = integer,
name = character,
age = numeric,
is_student = logical
)
)
# Create a data frame
<- PersonFrame(
persons id = 1:3,
name = c("Alice", "Bob", "Charlie"),
age = c(25, 30, 35),
is_student = c(TRUE, FALSE, TRUE)
)
print(persons)
#> Typed Data Frame Summary:
#> Base Frame Type: data.frame
#> Dimensions: 3 rows x 4 columns
#>
#> Column Specifications:
#> id : integer
#> name : character
#> age : numeric
#> is_student : logical
#>
#> Frame Properties:
#> Freeze columns : Yes
#> Allow NA : Yes
#> On violation : error
#>
#> Data Preview:
#> id name age is_student
#> 1 1 Alice 25 TRUE
#> 2 2 Bob 30 FALSE
#> 3 3 Charlie 35 TRUE
# Invalid modification (throws error)
try(persons$id <- letters[1:3])
#> Error : Property 'id' must be of type integer
<- type.frame(
PersonFrame frame = data.frame,
col_types = list(
id = integer,
name = character,
age = numeric,
is_student = logical,
gender = enum("M", "F"),
email = function(x) all(grepl("^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$", x)) # functions are applied to whole column
),freeze_n_cols = FALSE,
row_callback = function(row) {
if (row$age >= 40) {
return(sprintf("Age must be less than 40 (got %d)", row$age))
}if (row$name == "Yanice") {
return("Name cannot be 'Yanice'")
}return(TRUE)
},allow_na = FALSE,
on_violation = "error"
)
<- PersonFrame(
df id = 1:3,
name = c("Alice", "Bob", "Charlie"),
age = c(25, 35, 35),
is_student = c(TRUE, FALSE, TRUE),
gender = c("F", "M", "M"),
email = c("alice@test.com", "bob_no_valid@test.com", "charlie@example.com")
)
print(df)
#> Typed Data Frame Summary:
#> Base Frame Type: data.frame
#> Dimensions: 3 rows x 6 columns
#>
#> Column Specifications:
#> id : integer
#> name : character
#> age : numeric
#> is_student : logical
#> gender : Enum(M, F)
#> email : custom function
#>
#> Frame Properties:
#> Freeze columns : No
#> Allow NA : No
#> On violation : error
#>
#> Data Preview:
#> id name age is_student gender email
#> 1 1 TRUE 1 TRUE TRUE TRUE
#> 2 1 TRUE 1 TRUE TRUE TRUE
#> 3 1 TRUE 1 TRUE TRUE TRUE
summary(df)
#> id name age is_student
#> Min. :1 Length:3 Min. :1 Mode:logical
#> 1st Qu.:1 Class :character 1st Qu.:1 TRUE:3
#> Median :1 Mode :character Median :1
#> Mean :1 Mean :1
#> 3rd Qu.:1 3rd Qu.:1
#> Max. :1 Max. :1
#> gender.Length gender.Class gender.Mode email
#> 1 -none- logical Length:3
#> 1 -none- logical Class :character
#> 1 -none- logical Mode :character
#>
#>
#>
# Invalid row addition (throws error)
try(rbind(df, data.frame(
id = 4,
name = "David",
age = 500,
is_student = TRUE,
email = "d@test.com"
)))#> Error in rbind(deparse.level, ...) : Number of columns must match
This package provides powerful tools for ensuring type safety and validation in R. By defining interfaces, enums, typed functions, and typed data frames, you can create robust and reliable data structures and functions with strict type constraints.
This vignette demonstrates the basic usage and capabilities of the package. For more details, refer to the package documentation and examples provided in the source code.