mjktfw mjktfw - 4 years ago 129
R Question

Function operators and function wrapping in R

Hadley's "Advanced R" gives us a brief insight into functional programming and possible applications of "function operators" . However, my concern is that I either do not fully understand how these simplify the code, or unwillingly follow some bad practices by using other solutions.

Instead of creating operators that expect an input to be a function, I create a wrapper, that takes an expression as its argument, and returns a complete output (instead of a function, that still needs to be called). See example:

# function operator
delay_by <- function(delay, f) {
function(...) {
Sys.sleep(delay)
f(...)
}
}

# simple wrapper
delay_by2 <- function(expr, delay) {
Sys.sleep(delay)
eval(expr)
}

delay_by(1, sqrt)(2)
delay_by2(sqrt(2), 1)

operator <- delay_by(1, sqrt)
wrapper <- function(x) delay_by2(sqrt(x), 1)

operator(2)
wrapper(2)


This approach still allows for feeding into functionals (sapply etc.) and applying memoisation. In addition, this supports piping with
%>%
.

lapply(1:10, function(x) sqrt(x) %>% delay_by2(1))


The potential drawback of this solution is that it does not create persistent execution environments to store variables maintaining function's state, but these may be created explicitly with
<<- new.env()
.

Is there any reason why I should consider my approach inferior? I am trying to avoid an unnecessary nesting.

Answer Source
  • One important software engineering aspect is that functions are easy to test and package, expressions not so much. So if you use a function approach you can write unit tests, and put it in a package. With the eval approach you are limited to your own REPL testing. Obviously we should all be doing "Test Driven Development", at least on our production code, and nothing should go out the door without a set of unit tests that cover as much of the code as economically possible. This makes life much easier for people (including you) who might need to make changes someday.

  • Also note that there may be compiler optimizations that can be done function-wide, but not on expressions. That would be true in other languages, but I don't know enough about how R's compiler works to say anything about that.

  • Another point that occurs to me is that the eval route is not really scalable, i.e. only practical for expressions up to a certain complexity. Using functions on the other hand allows your logic to grow to any practical size.

  • Finally, eval as a construct is a very powerful one, but one that can get you in trouble, kind of like using a high-powered laser to cut paper. Since it is often easy to implement, it exists in many languages, and there are a lot of articles on the net about why it is dangerous - you should look at them. So like that high-powered laser, you shouldn't really get in the habit of using it except where it is necessary.

Recommended from our users: Dynamic Network Monitoring from WhatsUp Gold from IPSwitch. Free Download