9

I tried implementing a function let with the following semantics:

> let(x = 1, y = 2, x + y)
[1] 3

… which is conceptually somewhat similar to substitute with the syntax of with.

The following code almost works (the above invocation for instance works):

let <- function (...) {
    args <- match.call(expand.dots = FALSE)$`...`
    expr <- args[[length(args)]]
    eval(expr,
         list2env(lapply(args[-length(args)], eval), parent = parent.frame()))
}

Note the nested eval, the outer to evaluate the actual expression and the inner to evaluate the arguments.

Unfortunately, the latter evaluation happens in the wrong context. This becomes apparent when trying to call let with a function that examines the current frame, such as match.call:

> (function () let(x = match.call(), x))()
Error in match.call() :
  unable to find a closure from within which 'match.call' was called

I thought of supplying the parent frame as the evaluating environment for eval, but that doesn’t work:

let <- function (...) {
    args <- match.call(expand.dots = FALSE)$`...`
    expr <- args[[length(args)]]
    parent <- parent.frame()
    eval(expr,
         list2env(lapply(args[-length(args)], function(x) eval(x, parent)),
                  parent = parent)
}

This yields the same error. Which leads me to the question: how exactly is match.call evaluated? Why doesn’t this work? And, how do I make this work?

Konrad Rudolph
  • 530,221
  • 131
  • 937
  • 1,214
  • Possibly relevant is this, from `?match.call`: "Calling 'match.call' outside a function without specifying 'definition' is an error." And then try `j <- function(x) x; j(match.call())` to see one place that error plays out. I haven't gamed this all out (and don't quite get what you're really trying to do), but this may be an error that's pretty specific to the odd way you're using `match.call()` in that call to an anonymous function. – Josh O'Brien May 01 '13 at 14:28
  • @Josh I don’t think it’s related. The error message is different, and the context from which I call `match.call` is *definitely* from within a function, if the wrong one. – Konrad Rudolph May 01 '13 at 14:30
  • I guess what I was hinting at is that `match.call()` has pretty unique scoping rules that turn out to be specially hard-wired for it down at the C level. (The code defining `do_matchcall`, including some interesting comments about how the function it's called from is recorded, is in `$R_SRC/src/main/unique.c`.) Unlike almost any other function, it's not clear to me that it's evaluation environment can be manipulated/set via a call to `eval()`. To simplify your problem, maybe first figure out why this doesn't work (and how it could be made to): `j <- function() eval(call("match.call")); j()`. – Josh O'Brien May 01 '13 at 15:56
  • @Josh Ah yes, agreed. Though R offers a surprising wealth of functions to access the scope and stack frames so I think that there might be a way to push this square peg into `eval`’s round hole. – Konrad Rudolph May 01 '13 at 15:58
  • That it does. For instance, this does work: `j <- function() do.call("match.call", list()); j()`, though I'd have thought it equivalent to the `j` in my last comment. Will be interested to see what someone comes up with. – Josh O'Brien May 01 '13 at 16:10

1 Answers1

6

Will this rewrite solve your problem?

let <- function (expr, ...) {
    expr  <- match.call(expand.dots = FALSE)$expr
    given <- list(...)
    eval(expr, list2env(given, parent = parent.frame()))
}

let(x = 1, y = 2, x + y)
# [1] 3
flodel
  • 87,577
  • 21
  • 185
  • 223
  • +1 -- Interesting that `match.call(expand.dots=FALSE)$"..."` and `list(...)` return different objects. – Josh O'Brien May 01 '13 at 17:21
  • The arguments are in the wrong order here, and yet this appears to work. Could you explain **why** this works? I.e. why we can pass the expression as the *last* argument and still get it bound to the first? That said, this expression has the first problem as my first approach in that it doesn’t capture the parent scope … you need to explicitly pass `parent = parent.frame()` to `list2env`, despite this being the parameter’s default value, otherwise this won’t work: `(function (x) let(y=2, x+y))(1)` – Konrad Rudolph May 01 '13 at 17:57
  • 2
    @KonradRudolph -- That's just a consequence or [R's rules/algorithm for matching arguments](http://stat.ethz.ch/R-manual/R-devel/doc/manual/R-lang.html#Argument-matching). First the named arguments (here `x=1` and `y=2`) are processed, and neither of them has a name that matches the single named formal (`expr`). Then, as mentioned at the link above " Any unmatched formal arguments are bound to unnamed supplied arguments, in order." Here, that means `x + y` gets bound to `expr`. – Josh O'Brien May 01 '13 at 18:09
  • @Josh Ah, of course. But that actually comes with a rather nasty consequence: I cannot use it with any expression which contains reference to an identifier `expr`. Hmm. I’ll need to change the argument name to something more obscure, it seems. – Konrad Rudolph May 01 '13 at 18:12
  • 2
    The use of the `.` prefix as in `.expr` is a pretty common approach to avoid such situations and keep meaningful variable names. – flodel May 01 '13 at 18:15
  • 2
    @KonradRudolph -- Even worse, the second rule on that linked list means that any argument named `e` or `ex` or `exp` will get matched to `expr`. Partial argument matching was IMHO a crummy design decision that's now +/- locked in by the many contributed functions that use it. – Josh O'Brien May 01 '13 at 18:15
  • more reason to use `.expr`! – flodel May 01 '13 at 18:16
  • 1
    Another question: why use `match.call(expand.dots = FALSE)$expr` instead of `substitute(expr)`? – Konrad Rudolph May 01 '13 at 19:08
  • 1
    @KonradRudolph -- Good question. The two **are** slightly different, if: (a) an object `expr` has been assigned to somewhere in the current evaluation environment (in which case `substitute()` will pick up that value rather than the value of the supplied argument) or (b) if you provide a default value for `expr` in the function definition and then supply no value for it in your function call. In case (b), `substitute(expr)` performs better, and case (a) can be easily avoided. Nitpicky differences, but interesting. – Josh O'Brien Jul 18 '13 at 21:53