ts_function method for defining ocaps

This commit is contained in:
Tom Elliott 2025-01-16 12:25:56 +13:00
parent 57cdc92a2a
commit cdbf155b57
10 changed files with 232 additions and 46 deletions

View File

@ -1,2 +1,3 @@
^LICENSE\.md$ ^LICENSE\.md$
^README\.Rmd$ ^README\.Rmd$
^Makefile$

View File

@ -15,4 +15,5 @@ Suggests:
Config/testthat/edition: 3 Config/testthat/edition: 3
Imports: Imports:
cli, cli,
js js,
rlang

View File

@ -1,9 +1,19 @@
# Generated by roxygen2: do not edit by hand # Generated by roxygen2: do not edit by hand
S3method(check_type,default)
S3method(check_type,ts_function)
S3method(check_type,ts_object)
S3method(get_type,default)
S3method(get_type,ts_function)
S3method(get_type,ts_object)
S3method(print,ts_function)
S3method(print,ts_object) S3method(print,ts_object)
S3method(ts_compile,character) S3method(ts_compile,character)
S3method(ts_compile,default) S3method(ts_compile,default)
S3method(ts_compile,ts_function) S3method(ts_compile,ts_function)
export(check_type)
export(get_type)
export(is_ts_object)
export(ts_character) export(ts_character)
export(ts_compile) export(ts_compile)
export(ts_dataframe) export(ts_dataframe)
@ -12,4 +22,6 @@ export(ts_function)
export(ts_integer) export(ts_integer)
export(ts_list) export(ts_list)
export(ts_logical) export(ts_logical)
export(ts_null)
export(ts_numeric) export(ts_numeric)
export(ts_void)

View File

@ -1,6 +1,5 @@
parse_args <- function(x, mc) { parse_args <- function(x, mc) {
fmls <- lapply(x, eval) fmls <- lapply(x, eval)
mc <- mc[-1]
if (!all(names(mc) %in% names(fmls))) { if (!all(names(mc) %in% names(fmls))) {
stop( stop(
"Invalid argument(s): ", "Invalid argument(s): ",
@ -49,7 +48,7 @@ ts_result <- function(type, value) {
#' @param ... argument definitions (only required if f does not specify these in its formals) #' @param ... argument definitions (only required if f does not specify these in its formals)
#' @param result return type (ignored if overloads are provided) #' @param result return type (ignored if overloads are provided)
#' @export #' @export
ts_function <- function(f, ..., result = NULL) { ts_function <- function(f, ..., result = ts_void()) {
args <- list(...) args <- list(...)
if (!is.null(result) && !is_ts_object(result)) { if (!is.null(result) && !is_ts_object(result)) {
stop("Invalid return type") stop("Invalid return type")
@ -59,13 +58,42 @@ ts_function <- function(f, ..., result = NULL) {
args <- lapply(formals(f), eval) args <- lapply(formals(f), eval)
} }
fn <- function(...) { e <- new.env()
mc <- match.call(f) e$f <- f
x <- parse_args(args, mc) # e$env <- env
result$check(do.call(f, x)) e$args <- args
e$result <- result
e$call <- function(...) {
mc <- match.call(e$f)
.args <- parse_args(args, mc[-1])
.res <- do.call(e$f, .args)
check_type(result, .res)
} }
attr(fn, "args") <- args
attr(fn, "result") <- result e$copy <- function(env = parent.frame()) {
class(fn) <- c("ts_function", class(f)) e2 <- e
fn environment(e2$f) <- rlang::env_clone(environment(e$f), env)
e2
}
class(e) <- "ts_function"
e
}
#' @export
print.ts_function <- function(x, ...) {
cli::cli_h3("Ocap function")
cli::cli_text("Arguments:")
args <- lapply(x$args, \(z) z$input_type)
lapply(names(args), \(n) {
cat("- ", n, ": ", args[[n]], "\n", sep = "")
})
cli::cli_text("\n\n")
cli::cli_text("Return type:")
cat(x$result$return_type)
} }

View File

@ -36,6 +36,7 @@ print.ts_object <- function(x, ...) {
} }
cli::cli_h3("Return type: ") cli::cli_h3("Return type: ")
if (nchar(x$return_type) > 50) { if (nchar(x$return_type) > 50) {
print(x$return_type)
cat(js::uglify_reformat(x$return_type, beautify = TRUE), "\n") cat(js::uglify_reformat(x$return_type, beautify = TRUE), "\n")
} else { } else {
cat(x$return_type, "\n") cat(x$return_type, "\n")
@ -43,12 +44,74 @@ print.ts_object <- function(x, ...) {
cli::cli_end() cli::cli_end()
} }
#' @describeIn object Check if an object is a ts object #' @describeIn ts_object Check if an object is a ts object
#' @export #' @export
is_ts_object <- function(x) { is_ts_object <- function(x) {
inherits(x, "ts_object") inherits(x, "ts_object")
} }
#' @describeIn ts_object Get the input type of a ts object
#' @param x A ts object
#' @param which Which type to get, either "input" or "return"
#' @export
get_type <- function(x, which = c("input", "return")) {
UseMethod("get_type")
}
#' @export
get_type.ts_object <- function(x, which = c("input", "return")) {
which <- match.arg(which)
if (which == "input") {
return(x$input_type)
}
x$return_type
}
#' @export
get_type.ts_function <- function(x, which = c("input", "return")) {
which <- match.arg(which)
if (which == "input") {
return("z.function()")
}
"Robj.ocap()"
}
#' @export
get_type.default <- function(x, which) {
stop("Invalid object")
}
#' @describeIn ts_object Check if an object has the correct type
#' @param type A ts object
#' @param x An object
#' @export
check_type <- function(type, x) UseMethod("check_type")
#' @export
check_type.default <- function(type, x) {
stop("Invalid object")
}
#' @export
check_type.ts_object <- function(type, x) {
type$check(x)
}
#' @export
check_type.ts_function <- function(type, x) {
if ("ts_function" %in% class(x)) {
return(x)
}
if (!is.function(x)) stop("Expected a function")
do.call(
ts_function,
c(
list(x), attr(type, "args"),
list(result = attr(type, "result"))
)
)
}
ts_union <- function(...) sprintf("z.union([%s])", paste(..., sep = ", ")) ts_union <- function(...) sprintf("z.union([%s])", paste(..., sep = ", "))
ts_array <- function(type = c("z.number()", "z.boolean()", "z.string()")) { ts_array <- function(type = c("z.number()", "z.boolean()", "z.string()")) {
if (type == "z.number()") { if (type == "z.number()") {
@ -182,8 +245,8 @@ ts_list <- function(...) {
type <- "z.union([z.object({}), z.array(z.any())])" type <- "z.union([z.object({}), z.array(z.any())])"
type_fn <- "" type_fn <- ""
if (length(values)) { if (length(values)) {
types <- sapply(values, function(x) x$input_type) types <- sapply(values, get_type, which = "input")
type_funs <- sapply(values, function(x) x$return_type) type_funs <- sapply(values, get_type, which = "return")
if (!is.null(names(values))) { if (!is.null(names(values))) {
type <- sprintf( type <- sprintf(
"{ %s }", "{ %s }",
@ -216,7 +279,7 @@ ts_list <- function(...) {
} }
} }
for (i in seq_along(values)) { for (i in seq_along(values)) {
values[[i]]$check(x[[i]]) x[[i]] <- check_type(values[[i]], x[[i]])
} }
} }
x x
@ -236,8 +299,8 @@ ts_dataframe <- function(...) {
type <- "z.record(z.string(), z.any())" type <- "z.record(z.string(), z.any())"
type_fn <- "" type_fn <- ""
if (length(values)) { if (length(values)) {
types <- sapply(values, function(x) x$input_type) types <- sapply(values, get_type, which = "input")
type_funs <- sapply(values, function(x) x$return_type) type_funs <- sapply(values, get_type, which = "return")
if (is.null(names(values))) stop("Expected named elements") if (is.null(names(values))) stop("Expected named elements")
type <- sprintf( type <- sprintf(
@ -259,3 +322,26 @@ ts_dataframe <- function(...) {
} }
) )
} }
#' @export
ts_null <- function() {
ts_object(
"z.null()",
"Robj.null()",
check = function(x) {
if (!is.null(x)) stop("Expected NULL")
x
}
)
}
#' @export
ts_void <- function() {
ts_object(
"z.void()",
"null",
check = function(x) {
return(NULL)
}
)
}

View File

@ -36,18 +36,24 @@ Writing functions is easy, just use the `ts_*()` functions to define formals and
```r ```r
library(ts) library(ts)
app <- ts_list( addFn <- ts_function(
add = ts_fun( function(a = ts_numeric(1), b = ts_numeric(1)) a + b,
function(x = ts_number(1), y = ts_number(1)) a + b, result = ts_numeric(1)
ts_numeric(1) )
), sampleFn <- ts_function(
sample = ts_fun( function(x = ts_character(), n = ts_integer(1)) sample(x, n),
function(x, n) { result = ts_character()
sample(x, n) )
}, app <- ts_function(
x = ts_string(), function() {
n = ts_number(1), list(
result = r_character() add = addFn,
sample = sampleFn
)
},
result = ts_list(
add = appFn,
sample = sampleFn
) )
) )

View File

@ -4,7 +4,7 @@
\alias{ts_function} \alias{ts_function}
\title{TS function definition} \title{TS function definition}
\usage{ \usage{
ts_function(f, ..., result = NULL) ts_function(f, ..., result = ts_void())
} }
\arguments{ \arguments{
\item{f}{an R function} \item{f}{an R function}

View File

@ -4,7 +4,10 @@
\alias{ts_list} \alias{ts_list}
\title{Typed list} \title{Typed list}
\usage{ \usage{
ts_list(values = NULL) ts_list(...)
}
\arguments{
\item{...}{A list of types, named or unnamed.}
} }
\description{ \description{
A list is a vector of other robjects, which may or may not be named. A list is a vector of other robjects, which may or may not be named.

View File

@ -1,16 +1,25 @@
% Generated by roxygen2: do not edit by hand % Generated by roxygen2: do not edit by hand
% Please edit documentation in R/types.R % Please edit documentation in R/types.R
\name{object} \name{ts_object}
\alias{object} \alias{ts_object}
\alias{is_ts_object}
\alias{get_type}
\alias{check_type}
\title{Typed object} \title{Typed object}
\usage{ \usage{
object( ts_object(
input_type = "any", input_type = "any",
return_type = "any", return_type = "any",
default = NULL, default = NULL,
check = function() stop("Not implemented"), check = function() stop("Not implemented"),
generic = FALSE generic = FALSE
) )
is_ts_object(x)
get_type(x, which = c("input", "return"))
check_type(type, x)
} }
\arguments{ \arguments{
\item{input_type}{The type of the object that Typescript expect to send to R.} \item{input_type}{The type of the object that Typescript expect to send to R.}
@ -22,7 +31,22 @@ object(
\item{check}{A function that checks the object and returns it if it is valid. This operates on the R side and is mostly for development and debugging purposes. It is up to the developer to ensure that all functions return the correct type of object always.} \item{check}{A function that checks the object and returns it if it is valid. This operates on the R side and is mostly for development and debugging purposes. It is up to the developer to ensure that all functions return the correct type of object always.}
\item{generic}{logical, if \code{TRUE} then the object is a generic type.} \item{generic}{logical, if \code{TRUE} then the object is a generic type.}
\item{x}{An object}
\item{which}{Which type to get, either "input" or "return"}
\item{type}{A ts object}
} }
\description{ \description{
This is the base type for all typed objects. It is not meant to be used directly. This is the base type for all typed objects. It is not meant to be used directly.
} }
\section{Functions}{
\itemize{
\item \code{is_ts_object()}: Check if an object is a ts object
\item \code{get_type()}: Get the input type of a ts object
\item \code{check_type()}: Check if an object has the correct type
}}

View File

@ -7,8 +7,8 @@ test_that("anonomous function definitions", {
result = ts_numeric(1) result = ts_numeric(1)
) )
expect_equal(add(1, 2), 3) expect_equal(add$call(1, 2), 3)
expect_error(add("a", 2)) expect_error(add$call("a", 2))
}) })
test_that("named function definitions", { test_that("named function definitions", {
@ -18,26 +18,51 @@ test_that("named function definitions", {
result = ts_numeric() result = ts_numeric()
) )
x <- sample_num(1:10) x <- sample_num$call(1:10)
expect_true(all(x %in% 1:10)) expect_true(all(x %in% 1:10))
expect_error(sample_num("a")) expect_error(sample_num("a"))
}) })
test_that("function with complex return types", { test_that("void return types", {
sampler <- ts_function( print_x <- ts_function(
function(x = ts_numeric()) { function(x = ts_numeric()) {
print(x)
return(NULL)
}
)
expect_output(z <- print_x$call(1:10))
expect_null(z)
})
test_that("function with complex return types", {
get_sample <- ts_function(
function(n = ts_numeric(1)) {
sample(values, n)
},
result = ts_numeric()
)
sampler <- ts_function(
function(values = ts_numeric()) {
list( list(
get = function(n) sample(x, n) get = get_sample$copy(),
set = ts_function(
function(value = ts_numeric()) {
values <<- value
}
)
) )
}, },
result = ts_list( result = ts_list(
get = ts_function( get = get_sample,
NULL, set = ts_function(NULL, value = ts_numeric())
n = ts_integer(1),
result = ts_numeric(1)
)
) )
) )
s <- sampler(1:10) s <- sampler$call(1:10)
expect_type(s$get$call(2), "integer")
expect_silent(s$set$call(100:200))
expect_gte(s$get$call(1), 100)
}) })