8

For example, suppose I would like to be able to define a function that returned the name of the assignment variable concatenated with the first argument:

a <- add_str("b")
a
# "ab"

The function in the example above would look something like this:

add_str <- function(x) {
  arg0 <- as.list(match.call())[[1]]
  return(paste0(arg0, x))
}

but where the arg0 line of the function is replaced by a line that will get the name of the variable being assigned ("a") rather than the name of the function.

I've tried messing around with match.call and sys.call, but I can't get it to work. The idea here is that the assignment operator is being called on the variable and the function result, so that should be the parent call of the function call.

Jaap
  • 81,064
  • 34
  • 182
  • 193
Wart
  • 540
  • 4
  • 9
  • Could you show an example of your expected behaviour? – Roman Oct 21 '18 at 17:55
  • 1
    This is not something you should need. If you need it, you are doing very advanced stuff or (more likely) your approach is flawed and you should reconsider the design. – Roland Oct 22 '18 at 10:24
  • I assume that you're just fooling around to learn more about R, but just in case, is there a real use case for this ? – moodymudskipper Oct 22 '18 at 17:47
  • @Moody_Mudskipper I asked this over a year ago and now I can't remember why. – Wart Oct 22 '18 at 21:09
  • @Moody_Mudskipper Maybe I wanted to automatically name the columns of a matrix based on the variable name. – Wart Oct 22 '18 at 21:28
  • Oh, I see, that would be much simpler to do... I hadn't noticed that the bounty wasn't from you (you'll have an opportunity to accept an answer though). Maybe @Jeffrey can tell us more about what he wants out of this ? – moodymudskipper Oct 22 '18 at 21:38
  • @Moody_Mudskipper I have a function that reads package data. If you call it without assignment, it loads that data to a default variable such as `x`. But if you use assignment, it loads it to what you assigned it as, e.g., `myfun(dat)` loads to `x` but `y <- myfun(dat)` loads to `y`. I thought it would be nice for transparency reasons (for newer R users) to be able to output a message such as `Data loaded into variable y.` or `Data loaded into variable x.` – Jeffrey Girard Oct 22 '18 at 22:21
  • 1
    I see... in this case you have a function that is sometimes having side effects (assigning to x variable by default), and sometimes returning a value. This can be confusing. Your function might be more consistent and clear to the user if you use non standard evaluation and call : `add_str("b", a)` to assign to `a`, when 2nd parameter is missing you assign to x, and you can write your message either way for full consistency, and the function returns `invisible(NULL)` or `TRUE`. A good name is important with side effect functions, starting with 'set' is common – moodymudskipper Oct 22 '18 at 22:46
  • (which is actually basically Yosi's answer) – moodymudskipper Oct 23 '18 at 07:42

4 Answers4

11

I think that it's not strictly possible, as other solutions explained, and the reasonable alternative is probably Yosi's answer.

However we can have fun with some ideas, starting simple and getting crazier gradually.


1 - define an infix operator that looks similar

`%<-add_str%` <- function(e1, e2) {
  e2_ <- e2
  e1_ <- as.character(substitute(e1))
  eval.parent(substitute(e1 <- paste0(e1_,e2_)))
}

a %<-add_str% "b" 
a
# "ab"

2 - Redefine := so that it makes available the name of the lhs to the rhs through a ..lhs() function

I think it's my favourite option :

`:=` <- function(lhs,rhs){
  lhs_name <- as.character(substitute(lhs))
  assign(lhs_name,eval(substitute(rhs)), envir = parent.frame())
  lhs
}

..lhs <- function(){
  eval.parent(quote(lhs_name),2)
}

add_str <- function(x){
  res <- paste0(..lhs(),x)
  res
}

a := add_str("b")
a
# [1] "ab"

There might be a way to redefine <- based on this, but I couldn't figure it out due to recursion issues.


3 - Use memory address dark magic to hunt lhs (if it exists)

This comes straight from: Get name of x when defining `(<-` operator

We'll need to change a bit the syntax and define the function fetch_name for this purpose, which is able to get the name of the rhs from a *<- function, where as.character(substitute(lhs)) would return "*tmp*".

fetch_name <- function(x,env = parent.frame(2)) {
  all_addresses       <- sapply(ls(env), pryr:::address2, env)
  all_addresses       <- all_addresses[names(all_addresses) != "*tmp*"]
  all_addresses_short <- gsub("(^|<)[0x]*(.*?)(>|$)","\\2",all_addresses)

  x_address       <- tracemem(x)
  untracemem(x)
  x_address_short <- tolower(gsub("(^|<)[0x]*(.*?)(>|$)","\\2",x_address))

  ind    <- match(x_address_short, all_addresses_short)
  x_name <- names(all_addresses)[ind]
  x_name
}

`add_str<-` <- function(x,value){
  x_name <- fetch_name(x)
  paste0(x_name,value)
}

a <- NA
add_str(a) <- "b"
a

4- a variant of the latter, using .Last.value :

add_str <- function(value){
  x_name <- fetch_name(.Last.value)
  assign(x_name,paste0(x_name,value),envir = parent.frame())
  paste0(x_name,value)
}

a <- NA;add_str("b")
a
# [1] "ab"

Operations don't need to be on the same line, but they need to follow each other.


5 - Again a variant, using a print method hack

Extremely dirty and convoluted, to please the tortured spirits and troll the others.

This is the only one that really gives the expected output, but it works only in interactive mode.

The trick is that instead of doing all the work in the first operation I also use the second (printing). So in the first step I return an object whose value is "b", but I also assigned a class "weird" to it and a printing method, the printing method then modifies the object's value, resets its class, and destroys itself.

add_str <- function(x){
  class(x) <- "weird"
  assign("print.weird", function(x) {
    env <- parent.frame(2)
    x_name <- fetch_name(x, env)
    assign(x_name,paste0(x_name,unclass(x)),envir = env)
    rm(print.weird,envir = env)
    print(paste0(x_name,x))
  },envir = parent.frame())
  x
}

a <- add_str("b")
a
# [1] "ab"

(a <- add_str("b") will have the same effect as both lines above. print(a <- add_str("b")) would also have the same effect but would work in non interactive code, as well.

moodymudskipper
  • 46,417
  • 11
  • 121
  • 167
5

This is generally not possible because the operator <- is actually parsed to a call of the <- function:

rapply(as.list(quote(a <- add_str("b"))), 
       function(x) if (!is.symbol(x)) as.list(x) else x,
       how = "list")
#[[1]]
#`<-`
#
#[[2]]
#a
#
#[[3]]
#[[3]][[1]]
#add_str
#
#[[3]][[2]]
#[1] "b"

Now, you can access earlier calls on the call stack by passing negative numbers to sys.call, e.g.,

 foo <- function() {
  inner <- sys.call()
  outer <- sys.call(-1)
  list(inner, outer)
}

print(foo())
#[[1]]
#foo()
#[[2]]
#print(foo())

However, help("sys.call") says this (emphasis mine):

Strictly, sys.parent and parent.frame refer to the context of the parent interpreted function. So internal functions (which may or may not set contexts and so may or may not appear on the call stack) may not be counted, and S3 methods can also do surprising things.

<- is such an "internal function":

`<-`
#.Primitive("<-")

`<-`(x, foo())
x
#[[1]]
#foo()
#
#[[2]]
#NULL
Roland
  • 127,288
  • 10
  • 191
  • 288
4

As Roland pointed, the <- is outside of the scope of your function and could only be located looking at the stack of function calls, but this fail. So a possible solution could be to redefine the '<-' else than as a primitive or, better, to define something that does the same job and additional things too. I don't know if the ideas behind following code can fit your needs, but you can define a "verbose assignation" :

`:=` <- function (var, value) 
{
    call = as.list(match.call())
    message(sprintf("Assigning %s to %s.\n",deparse(call$value),deparse(call$var)))
    eval(substitute(var <<- value))
    return(invisible(value))
 }

x := 1:10
# Assigning 1:10 to x.
x
# [1]  1  2  3  4  5  6  7  8  9 10

And it works in some other situation where the '<-' is not really an assignation :

y <- data.frame(c=1:3)
colnames(y) := "b"
# Assigning "b" to colnames(y).
y
#  b
#1 1
#2 2
#3 3

z <- 1:4
dim(z) := c(2,2)
#Assigning c(2, 2) to dim(z).
z
#     [,1] [,2]
#[1,]    1    3
#[2,]    2    4

>

Nicolas2
  • 2,170
  • 1
  • 6
  • 15
3

I don't think the function has access to the variable it is being assigned to. It is outside of the function scope and you do not pass any pointer to it or specify it in any way. If you were to specify it as a parameter, you could do something like this:

add_str <- function(x, y) {
  arg0 <-deparse(substitute(x))
  return(paste0(arg0, y))
}

a <- 5
add_str(a, 'b')
#"ab"
Yosi Hammer
  • 588
  • 2
  • 8