5

Background

I’m in the process of creating a shortcut for lambdas, since the repeated use of function (…) … clutters my code considerably. As a remedy, I’m trying out alternative syntaxes inspired by other languages such as Haskell, as far as this is possible in R. Simplified, my code looks like this:

f <- function (...) {
    args <- match.call(expand.dots = FALSE)$...
    last <- length(args)
    params <- c(args[-last], names(args)[[last]])

    function (...)
        eval(args[[length(args)]],
             envir = setNames(list(...), params),
             enclos = parent.frame())
}

This allows the following code:

f(x = x * 2)(5)       # => 10
f(x, y = x + y)(1, 2) # => 3

etc.

Of course the real purpose is to use this with higher-order functions1:

Map(f(x = x * 2), 1 : 10)

The problem

Unfortunately, I sometimes have to nest higher-order functions and then it stops working:

f(x = Map(f(y = x + y), 1:2))(10)

yields “Error in eval(expr, envir, enclos): object x not found”. The conceptually equivalent code using function instead of f works. Furthermore, other nesting scenarios also work:

f(x = f(y = x + y)(2))(3) # => 5

I’m suspecting that the culprit is the parent environment of the nested f inside the map: it’s the top-level environment rather than the outer f’s. But I have no idea how to fix this, and it also leaves me puzzled that the second scenario above works. Related questions (such as this one) suggest workarounds which are not applicable in my case.

Clearly I have a gap in my understanding of environments in R. Is what I want possible at all?


1 Of course this example could simply be written as (1 : 10) * 2. The real application is with more complex objects / operations.

Community
  • 1
  • 1
Konrad Rudolph
  • 530,221
  • 131
  • 937
  • 1,214
  • Could you just make your function(s) only use input variables? E.g. `f(x = Map(f(x, y = x + y), x, 1:2))(10)` – flodel Dec 15 '13 at 16:59
  • @flodel The thing is: *why* does your workaround work? That `x` isn’t global either, is it? It’s part of the arguments passed to `Map` from within the outer `f`. And of course I would love to be able to avoid this client-side workaround, and exactly replicate how `function`s work. – Konrad Rudolph Dec 15 '13 at 17:02
  • Is there a reason you are not using the 'lambda.r' package? (I suspect the reason you have difficulty is that functions in R carry their environment of construction with them ... so-called lexical scoping rather than dynamic scoping.) – IRTFM Dec 15 '13 at 17:17
  • @DWin Yes, `lambda.r` doesn’t quite do the same that I want. In particular, its syntax, while very interesting in itself, isn’t quite terse enough to distinguish it significantly from the default `function`. It’s only three characters shorter, and it declares a *named* function (despite the package’s name). – Konrad Rudolph Dec 15 '13 at 17:34
  • https://github.com/hadley/pryr/blob/master/R/f.r ? – hadley Dec 17 '13 at 02:05
  • You might also want to check http://adv-r.had.co.nz/Function-operators.html and http://adv-r.had.co.nz/Expressions.html#creating-a-function – hadley Dec 17 '13 at 02:07
  • @hadley I’ve read that of course, and I’m aware of your `f.r` (but I prefer my interface). However, I’ll have a look at it for the actual implementation. – Konrad Rudolph Dec 17 '13 at 20:52

2 Answers2

5

The answer is to attach parent.frame() to the output function's environment:

f <- function (...) {
    args <- match.call(expand.dots = FALSE)$...
    last <- length(args)
    params <- c(args[-last], names(args)[[last]])

    e <- parent.frame()

    function (...)
        eval(args[[length(args)]],
             envir = setNames(list(...), params),
             enclos = e)
}

Hopefully someone can explain well why this works and not yours. Feel free to edit.

flodel
  • 87,577
  • 21
  • 185
  • 223
  • I think it works because it is explicitly overwriting the evaluation environment that would have been established at the time of function definition. It's not really attaching a new frame so much as it is establishing a mechanism to replace the original frame when the function is called. – IRTFM Dec 15 '13 at 17:34
  • Hmm, I have to think about this some more. – Konrad Rudolph Dec 15 '13 at 18:24
  • @DWin -- Maybe along the same lines of what you're saying, the key seems to be that flodel's code fixes the enclosing environment at the time of function definition, rather than leaving it 'til later at the time of function evaluation. (I'm of course referring there to definition and evaluation of the function returned by `f()`). For more detail, including an explanation of why `parent.frame(n=1)` in his answer points to the appropriate evaluation frame on the call stack, see the answer I just added below. – Josh O'Brien Dec 16 '13 at 18:23
4

Great question.

Why your code fails

Your code fails because eval()'s supplied enclos= argument does not point far enough up the call stack to reach the environment in which you are wanting it to next search for unresolved symbols.

Here is a partial diagram of the call stack from the bottom of which your call to parent.frame() occurs. (To make sense of this, it's important to keep in mind that the function call from which parent.frame() is here being called is not f(), but a call the anonymous function returned by f() (let's call it fval)).

## Note: E.F. = "Evaluation Frame"
##       fval = anonymous function returned as value of nested call to f()

f(   <-------------------------  ## E.F. you want, ptd to by parent.frame(n=3)      
  Map(
      mapply(   <--------------------  ## E.F. pointed to by parent.frame(n=1)
             fval(                  |
                  parent.frame(n=1  |

In this particular case, redefining the function returned by f() to call parent.frame(n=3) rather than parent.frame(n=1) produces working code, but that's not a good general solution. For instance, if you wanted to call f(x = mapply(f(y = x + y), 1:2))(10), the call stack would then be one step shorter, and you'd instead need parent.frame(n=2).

Why flodel's code works

flodel's code provides a more robust solution by calling parent.frame() during evaluation of the inner call to f in the nested chain f(Map(f(), ...)) (rather than during the subsequent evaluation of the anonymous function fval returned by f()).

To understand why his parent.frame(n=1) points to the appropriate environment, it's important to recall that in R, supplied arguments are evaluated in the the evaluation frame of the calling function. In the OP's example of nested code, the inner f() is evaluated during the processing of Map()'s supplied arguments, so it's evaluation environment is that of the function calling Map(). Here, the function calling Map() is the outer call to f(), and its evaluation frame is exactly where you want eval() to next be looking for symbols:

f(   <--------------------- ## Evaluation frame of the nested call to f()
  Map(f(                  |
        parent.frame(n=1  |
    
Community
  • 1
  • 1
Josh O'Brien
  • 159,210
  • 26
  • 366
  • 455