Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

strictly() #275

Closed
egnha opened this issue Dec 13, 2016 · 7 comments
Closed

strictly() #275

egnha opened this issue Dec 13, 2016 · 7 comments

Comments

@egnha
Copy link
Contributor

egnha commented Dec 13, 2016

A common form of boilerplate code at the top of functions is argument checking: You make some checks on the arguments, signal a condition if any show-stopping checks fail, then move on to the meat of the function if everything is good. The problem with this approach is that it can clutter up the main work of a function with admin; it spoils the "fun" of a function with the inconvenience of a security check.

A function strictly(), as a cohort of safely() and possibly(), would separate these concerns, functionally. It could have the signature

strictly(.f, ..., cond = NULL)

where .f is an interpreted function and cond is a condition object (if not NULL). The main part is ..., which consists of two-sided formulas p ~ m that specify the argument checks for .f:

  • p is an expression that is a call to a predicate function (the "check")
  • m is an expression whose evaluation can be coerced to a string (the "failure message" if the check fails)

The value of strictly(.f, ...) would be a function that applies .f "strictly."

Example:

foo <- function(f, x) x - f(x)

is_trig <- function(f) any(map_lgl(c(cos, sin, tan), identical, y = f))
chk_trig <- is_trig(f) ~ "f not trigonometric"
foo_s <- strictly(foo, is.numeric(x) ~ "x not numeric", chk_trig)

foo_s(sin, 2)    # [1] 1.090703
foo_s(log, 2)    # Error in foo_s(log, 2) : f not trigonometric
foo_s(cos, "a")  # Error in foo_s(cos, "a") : x not numeric
foo_s(log, "a")  # Error in foo_s(log, "a") : f not trigonometric; x not numeric

(While the error messages could be more informative, the basic utility is clear.)

Here's a basic implementation, with a modicum of metaprogramming:

strictly <- function(.f, ..., cond = NULL) {
  ..cond <- cond %||% identity
  ..chks <- list(...)

  .is_fml <- map_lgl(..chks, is_formula)
  if (!(is_function(.f) && all(.is_fml))) {
    stop("Invalid arguments for strictly()", call. = FALSE)
  }

  # Use two-sided `..` to make clashes with names in body(.f) improbable
  .body <- substitute({
    ..env.. <- as.list(environment())
    ..is_pass.. <- map_lgl(chks, lazyeval::f_eval_lhs, data = ..env..)
    if (!all(..is_pass..)) {
      ..msg.. <- chks[!..is_pass..] %>%
        map_chr(~ as.character(lazyeval::f_eval_rhs(.x, ..env..))) %>%
        paste(collapse = "; ")
      stop(..cond(..msg..))
    }
    body
  }, list(body = body(.f), chks = ..chks))

  .f_strict <- eval(call("function", formals(.f), as.call(.body)))
  environment(.f_strict) <- list2env(as.list(environment(.f), all.names = TRUE),
                                     parent = parent.env(environment(.f)))
  environment(.f_strict)$..cond <- ..cond
  environment(.f_strict)$..chks <- ..chks

  .f_strict
}

Alternatively, one could implement strictly() as a function-composition. However, the above implementation has the advantage of preserving the argument signature of .f, as well as its source code and environment.

Would strictly() be a meaningful addition to purrr?

@egnha
Copy link
Contributor Author

egnha commented Dec 13, 2016

Keeping with the principle of separating concerns, it would be handy if strictly(.f, ...) belonged to a class, say, "strict_closure" (inheriting from "function"), whose print method shows both the body of .f and the checks, separately.

@smbache
Copy link
Member

smbache commented Dec 13, 2016

It seems ensurer::ensure_that does pretty much what you're asking for?

https://cran.r-project.org/web/packages/ensurer/vignettes/ensurer.html

@egnha
Copy link
Contributor Author

egnha commented Dec 13, 2016

@smbache I was unaware of ensurer—wish I had known about it earlier! Thanks very much for the pointer. It's encouraging that I had (unwittingly) rediscovered your method of employing formulas of the form <condition> ~ <failure message> to devise custom error reporting. (Actually, in my case, "rediscovered" is taking too much credit for ideas that are directly stolen from @hadley'slazyeval package.) In any case, I take that coincidence as an indication that the interface for strictly is not wholly ill-conceived ...

What I understand of ensurer::ensure_that (from a cursory reading of your vignette, mind you), is that it's a consistent, reusable framework for validating objects by rather general criteria. Of course, such a framework can also be used to validate arguments to a function, by intercepting them with ensure before feeding them into a function, as you showed in your iris example. In that sense, ensure would render the strictly I've suggested superfluous.

In another sense, however, I think the value of strictly is that it brings a more functional emphasis to the problem, in line with purrr's functional operators safely and possibly: To add argument validation to a function, apply an appropriate functional transformation. As a bonus of this approach, you get back a function with its signature and environment preserved . (When you'd commented, I'd overlooked the issue of function environments, but went back to edit my implementation to rectify that omission.) I believe this is possible but not as straightforward to do with ensure.

Have I understood the role and function of ensurer, and its comparison to strictly, correctly?

@egnha
Copy link
Contributor Author

egnha commented Dec 14, 2016

In addition to a print method for the class strict_closure (class of values of strictly), it would be useful to consider three more:

  • get_checks(.f_strict), get_cond(.f_strict): Get the list of checks, resp. condition, of .f_strict, i.e., extract the list of formulas ..chks, resp. condition object ..cond, from the environment of .f_strict.

  • nonstrictly(.f_strict): Extract the underlying "non-strict" function. This is the inverse of strictly. (Not 100% happy with this name: nonstrictly is unambiguous, but not very evocative. Alternatives?)

Both strictly and nonstrictly would edit the header of a function, either by inserting validators or collapsing them, so this part could be implemented using a common constructor, say modify_fheader (facilitated, perhaps, by the use special delimiting comments).

@hadley
Copy link
Member

hadley commented Dec 14, 2016

This is a big problem so I think it's out of scope for purrr. Also see https://github.com/jimhester/types

@hadley hadley closed this as completed Dec 14, 2016
@egnha
Copy link
Contributor Author

egnha commented Dec 14, 2016

@hadley I guess I need to do my homework! Didn't know about these packages, either. Thanks.

Both Types for R, and the related package Declarative function argument check, tackle the larger problem. The second one especially is quite close in implementation to what I've suggested, but uses roxygen comments, rather than formulae, to specify checks. Relying on a comments-based mechanism is not as flexible as using formulae, à la lazyeval.

strictly has no pretension of solving argument checking once and for all, but rather to provide a simple functional version of it to cover common use cases, without much fuss. I thought it fit in harmoniously with safely and possibly, as providing a kind of "dual" functionality to these (checking/acting on the input vs. the output).

But you are right. The basic problem is big and nontrivial, and I do see the rationale behind leaving strictly—or something analogous—out of purrr. (Even a "poor man's" version of it.)

@egnha
Copy link
Contributor Author

egnha commented Jan 31, 2017

@hadley You're right, strictly() is outside the scope of purrr. In the meantime, I've implemented it as a stand-alone package called valaddin. strictly() acts as a functional operator, rather than as a function declaration, so its utility is distinct from that of either ensurer, typeCheck or argufy. In particular, it's more suitable for use in pipes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants