diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..895931c --- /dev/null +++ b/Makefile @@ -0,0 +1,13 @@ +default: README.md + +rfiles: $(wildcard R/*.R) + +document: + Rscript -e "devtools::document()" + +install: document + R CMD INSTALL . + +README.md: README.Rmd install + Rscript -e "rmarkdown::render('README.Rmd')" + @rm README.html diff --git a/NAMESPACE b/NAMESPACE index 6ae9268..df530a1 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -1,2 +1,9 @@ # Generated by roxygen2: do not edit by hand +S3method(print,ts_object) +export(ts_character) +export(ts_compile) +export(ts_function) +export(ts_integer) +export(ts_logical) +export(ts_numeric) diff --git a/R/compile.R b/R/compile.R new file mode 100644 index 0000000..8f8f40e --- /dev/null +++ b/R/compile.R @@ -0,0 +1,11 @@ +#' @export +ts_compile <- function(f) { + name <- deparse(substitute(f)) + inputs <- attr(f, "args") + result <- attr(f, "result") + + inputs <- sapply(inputs, \(x) x$type) + fn_args <- paste(names(inputs), inputs, sep = ": ") |> + paste(collapse = ", ") + cat(sprintf("const %s = (%s) => %s;", name, fn_args, result$type_fn), "\n") +} diff --git a/R/function.R b/R/function.R new file mode 100644 index 0000000..400a7fc --- /dev/null +++ b/R/function.R @@ -0,0 +1,34 @@ +parse_args <- function(x, mc) { + fmls <- lapply(x, eval) + mc <- mc[-1] + if (!all(names(mc) %in% names(fmls))) { + stop( + "Invalid argument(s): ", + paste(setdiff(names(mc), names(fmls)), collapse = ", ") + ) + } + args <- lapply(names(fmls), function(n) { + fmls[[n]]$check(eval(mc[[n]])) + }) + names(args) <- names(fmls) + args +} + +# TS function +#' @export +ts_function <- function(f, ..., result = NULL) { + args <- list(...) + if (!is_object(result)) { + stop("Invalid return type") + } + + fn <- function(...) { + mc <- match.call() + x <- parse_args(args, mc) + result$check(do.call(f, x)) + } + attr(fn, "args") <- args + attr(fn, "result") <- result + class(fn) <- c("ts_function", class(f)) + fn +} diff --git a/R/types.R b/R/types.R new file mode 100644 index 0000000..bb861de --- /dev/null +++ b/R/types.R @@ -0,0 +1,92 @@ +object <- function(type = "any", + type_fn = "any", + default = NULL, + check = function() stop("Not implemented")) { + e <- environment() + + e$attr <- function(name, value) { + e$attributes[[name]] <- value + } + + class(e) <- c("ts_object", class(e)) + e +} + +#' @export +print.ts_object <- function(x, ...) { + # name <- deparse(substitute(x)) + cat(sprintf("Object: %s\n", x$type)) + cat(sprintf("rserve-ts type: %s\n", x$type_fn)) +} + +is_object <- function(x) { + inherits(x, "ts_object") +} + +ts_union <- function(...) paste(..., sep = " | ") +ts_array <- function(type) paste(type, "[]", sep = "") + +n_type <- function(n, type, pl = ts_array(type)) { + if (n == 1) { + return(type) + } + if (n == -1) { + return(ts_union(type, pl)) + } + pl +} +n_type_fun <- function(n, type) { + sprintf("%s(%s)", type, ifelse(n < 0, "", n)) +} + +#' @export +ts_logical <- function(n = -1L) { + object( + n_type(n, "boolean"), + n_type_fun(n, "logical"), + check = function(x) { + if (!is.logical(x)) stop("Expected a boolean") + if (n > 0 && length(x) != n) stop("Expected a boolean of length ", n) + x + } + ) +} + +#' @export +ts_integer <- function(n = -1L) { + object( + n_type(n, "number"), + n_type_fun(n, "integer"), + check = function(x) { + if (!is.integer(x)) stop("Expected an integer") + if (n > 0 && length(x) != n) stop("Expected an integer of length ", n) + x + } + ) +} + +#' @export +ts_numeric <- function(n = -1L) { + object( + n_type(n, "number"), + n_type_fun(n, "numeric"), + check = function(x) { + if (!is.numeric(x)) stop("Expected a number") + if (n > 0 && length(x) != n) stop("Expected a number of length ", n) + x + } + ) +} + +#' @export +ts_character <- function(n = -1L) { + object( + n_type(n, "string"), + n_type_fun(n, "character"), + check = function(x) { + if (!is.character(x)) stop("Expected a string") + if (n > 0 && length(x) != n) stop("Expected a string of length ", n) + x + } + ) +} diff --git a/README.Rmd b/README.Rmd index d3ddf05..80a7ba3 100644 --- a/README.Rmd +++ b/README.Rmd @@ -31,32 +31,36 @@ devtools::install_github("tmelliott/ts") ## Example -Writing functions is easy, just use the `ts::x()` functions to define formals and return types. +Writing functions is easy, just use the `ts_*()` functions to define formals and return types. -*Note: we recommend not importing the library, and instead using the fully qualified name `ts::x()` to avoid conflicts with other libraries.* ```r -app <- ts::app( - add = ts::fun( - function(x = ts::number(), y = ts::number()) { - result <- x + y - ts::result(result, ts::number()) - } +library(ts) +app <- ts_app( + add = ts_fun( + function(x, y) { + x + y + }, + x = ts_number(), + y = ts_number(), + # ideally this will use a generic type where x OR y can be vectors + # and, if one is a vector, the return type will be a vector too... + result = ts_number() ), - sample = ts::fun( - function(x = ts::character_vector(), n = ts::integer()) { - result <- sample(x, n) - ts::result(result, - ts::condition(n, - 1 = ts::character(), - ts::character_vector() - ) - ) - } + sample = ts_fun( + function(x, n) { + sample(x, n) + }, + x = ts_character_vector(), + n = ts_integer(), + result = ts_condition(n, + 1 = ts_character(), + ts_character_vector() + ) ) ) -ts::compile(app) +ts_compile(app) ``` This will generate the following rserve-ts function definitions: @@ -85,3 +89,14 @@ type App = { Promise<{ data: N extends 1 ? string : string[] }>; }; ``` + +## State of the project + +Here's what's currently working: + +```{r} +library(ts) + +myfun <- ts_function(mean, x = ts_numeric(), result = ts_numeric()) +ts_compile(myfun) +``` diff --git a/README.html b/README.html deleted file mode 100644 index 5f737c3..0000000 --- a/README.html +++ /dev/null @@ -1,663 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - -

ts

- - - - -

The ts package makes it easy for users to write functions that can be used in rserve-ts applications.

-

Installation

-

You can install the development version of ts from GitHub with:

-
# install.packages("devtools")
-devtools::install_github("tmelliott/ts")
-

Example

-

Writing functions is easy, just use the ts::x() functions to define formals and return types.

-

Note: we recommend not importing the library, and instead using the fully qualified name ts::x() to avoid conflicts with other libraries.

-
app <- ts::app(
-  add = ts::fun(
-    function(x = ts::number(), y = ts::number()) {
-      result <- x + y
-      ts::result(result, ts::number())
-    }
-  ),
-  sample = ts::fun(
-    function(x = ts::character_vector(), n = ts::integer()) {
-      result <- sample(x, n)
-      ts::result(result,
-        ts::condition(n,
-          1 = ts::character(),
-          ts::character_vector()
-        )
-      )
-    }
-  )
-)
-
-ts::compile(app)
-

This will generate the following rserve-ts function definitions:

-
import { types as R } from "rserve-ts";
-
-export const app = {
-  add: z.function(
-    z.tuple([z.number(), z.number()]),
-    z.promise(R.numeric(1))
-  ),
-  sample: z.function(
-    z.tuple([z.character_vector(), z.integer()]),
-    z.promise(R.character())
-  )
-};
-

which will generate the following types:

-
type App = {
-  add: (x: number, y: number) => Promise<{ data: number }>;
-  sample: (x: string[], n: number) => Promise<{ data: string | string[] }>;
-  // or, if possible, even better:
-  sample: <N extends number>(x: string[], n: N) =>
-    Promise<{ data: N extends 1 ? string : string[] }>;
-};
- - - diff --git a/README.md b/README.md index 5984d6c..ec3dd30 100644 --- a/README.md +++ b/README.md @@ -22,34 +22,36 @@ devtools::install_github("tmelliott/ts") ## Example -Writing functions is easy, just use the `ts::x()` functions to define +Writing functions is easy, just use the `ts_*()` functions to define formals and return types. -*Note: we recommend not importing the library, and instead using the -fully qualified name `ts::x()` to avoid conflicts with other libraries.* - ``` r -app <- ts::app( - add = ts::fun( - function(x = ts::number(), y = ts::number()) { - result <- x + y - ts::result(result, ts::number()) - } +library(ts) +app <- ts_app( + add = ts_fun( + function(x, y) { + x + y + }, + x = ts_number(), + y = ts_number(), + # ideally this will use a generic type where x OR y can be vectors + # and, if one is a vector, the return type will be a vector too... + result = ts_number() ), - sample = ts::fun( - function(x = ts::character_vector(), n = ts::integer()) { - result <- sample(x, n) - ts::result(result, - ts::condition(n, - 1 = ts::character(), - ts::character_vector() - ) - ) - } + sample = ts_fun( + function(x, n) { + sample(x, n) + }, + x = ts_character_vector(), + n = ts_integer(), + result = ts_condition(n, + 1 = ts_character(), + ts_character_vector() + ) ) ) -ts::compile(app) +ts_compile(app) ``` This will generate the following rserve-ts function definitions: @@ -80,3 +82,15 @@ type App = { Promise<{ data: N extends 1 ? string : string[] }>; }; ``` + +## State of the project + +Here’s what’s currently working: + +``` r +library(ts) + +myfun <- ts_function(mean, x = ts_numeric(), result = ts_numeric()) +ts_compile(myfun) +#> const myfun = (x: number | number[]) => numeric(); +```