egnha / valaddin

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

Grouping parameters with validation declarations? #19

Closed MilkWasABadChoice closed 7 years ago

MilkWasABadChoice commented 7 years ago

Valaddin has exactly the semantics I've been looking for – the only drawback is that I can't find a way to leverage it without sacrificing readability and locality of semantics in code. When I'm defining a function in an explicitly typed language, it's easy for someone to see the intent by reading the function signature:

def doSomething(a: Numeric, b: Numeric): Numeric = ...

With valaddin, I'm stuck doing something like this:

doSomething <- function(a, b) {
  ...
  ... potentially many lines of code in between ...
  ...
 }

doSomething %<>% firmly(list(~a, ~b) ~ { is.numeric(a) && is.numeric(b)})`

I'm reluctant to adopt any convention that forces the reader to go all the way past the end of a function to find out the key information they need to understand its arguments. The only option I've come up with would be introducing a "reverse" magrittr-style %<% operator that pulls from right-to-left (haven't tried this, just hypothetically):

doSomething <- firmly(list(~a, ~b) ~ { ... }) %<% function(a, b) {
  ... function body goes here ...
}

Apologies if this is stretching the nature of "Issue" into "Stylistic Grievance", but I really like Valaddin's design & want to adopt it. Is there anything you can suggest to make it possible to co-locate the function signature/declaration and the declaration of the semantic constraints, or at least allow them to be close enough that the reader's job of understanding the code isn't frustrated by not knowing what semantic expectations may or may not be intended by the code's author?

egnha commented 7 years ago

Good question, thanks. There is a .checklist argument in firmly that accepts checks in a list, which you can use as a receptacle when delivering checks via %>%. For example, if you want to add the check that a and b are numeric vectors of equal length and you want to keep these checks close to the function header, you can do the following:

library(magrittr)
library(valaddin)

vec_add <- list(
  list(~a, ~b) ~ is.numeric,
  list(~length(a) == length(b)) ~ isTRUE
) %>%
  firmly(
    function(a, b) {
      a + b
    },
    .checklist = .
  )

In this case, you can express the checks more succinctly (since is.numeric is imposed "globally", on all arguments):

vec_add <- list(
  ~is.numeric,  # Alternatively, globalize(vld_numeric)
  vld_true(~length(a) == length(b))
) %>% 
  firmly(
    function(a, b) {
      a + b
    },
    .checklist = .
  )

In fact, you're better off not using the pipe, at all (shorter and reads the same):

vec_add <- firmly(
  ~is.numeric,
  vld_true(~length(a) == length(b)),
  .f = function(a, b) {
    a + b
  }
)
egnha commented 7 years ago

@MilkWasABadChoice It is possible to do "reverse magrittr" for input validation, by defining a new operator (for which %<% would not be the best name):

`%firmly%` <- function(.checklist, .f) firmly(.f, .checklist = .checklist)

vec_add <- list(
  ~is.numeric, vld_true(~length(a) == length(b))
) %firmly% 
  function(a, b) {
    a + b
  }

It'd be useful to have such an operator built into valaddin. Excellent idea, thanks!