egnha / valaddin

Functional input validation to make R functions more readable and robust
Other
33 stars 1 forks source link

Evaluate check items as quosures #39

Closed egnha closed 7 years ago

egnha commented 7 years ago

This is already working with lazyeval::f_list() and lazyeval::f_new(), but should be replaced by equivalent functions in rlang (quos(), new_quosure(), resp.).

egnha commented 7 years ago

The API for input validation checks has to change: we should use quosures instead formulas, so that we can use tidyeval for the construction of check expressions while (crucially) still using standard evaluation to perform input validation (for performance reasons).

The new API will reserve the use of formulas as a mechanism to pair predicates with their scope of application, but not in order to capture an eval-environment, which is the job of the quosure.

Sketch of new input validation API:

library(rlang)

msg1 <- "Not positive"
msg2 <- "Not greater than 1"
predicate <- is.vector
a <- 1
zero <- 0

# re-export quos for valaddin
chks <- quos(
  # predicates are "bare"
  is.numeric,
  !!predicate,
  # retain lambda-expressions
  !!msg1 := {. > UQ(zero)},
  # string interpolation via glue(); scope now goes on the RHS of formula
  "{.} is not an integer" = is.integer ~ x,
  # quasiquotation possible with quosures
  {. > 0} ~ quos(!!msg2 := x - !!a, y)
)

gives

chks

#> [[1]]
#> <quosure: global>
#>   ~is.numeric
#> 
#> [[2]]
#> <quosure: empty>
#>   ~function (x, mode = "any") 
#>     .Internal(is.vector(x, mode))
#> 
#> $`Not positive`
#> <quosure: global>
#>   ~{
#>     . > 0
#>   }
#> 
#> $`{.} is not an integer`
#> <quosure: global>
#>   ~(is.integer ~ x)
#> 
#> [[5]]
#> <quosure: global>
#>   ~({
#>     . > 0
#>   } ~ quos(`:=`("Not greater than 1", x - 1), y))
#> 
#> attr(,"class")
#> [1] "quosures"

chks can then be parsed and feed into the existing input-validation procedure via validating_closure() (with some minor tweaks).

egnha commented 7 years ago

Formulas are superfluous, as a pairing mechanism, for we can just use quos() within quos(), together with the convention that the first argument is the predicate function.

Example:

a <- 1
msg <- "Not positive"
local_msg <- "Not greater than {a}"

quos(
  # global check (which is to get an auto-gen message)
  is.numeric,
  # local check (with custom message, overridden by local messages)
  !!msg := quos({. > 0}, x, !!local_msg := y - !!a)
)

#> [[1]]
#> <quosure: global>
#>   ~is.numeric
#> 
#> $`Not positive`
#> <quosure: global>
#>   ~quos({
#>     . > 0
#>   }, x, `:=`("Not greater than {a}", y - 1))
#> 
#> attr(,"class")
#> [1] "quosures"
egnha commented 7 years ago

Implemented in the quosures branch.