Is it possible to have `dput` return source code that would run outside of the enclosing environment?

147 Views Asked by At

Suppose I have a closure add_y(y) which returns a function that adds y to its input.

add_y <- function(y) {
  function(x) {
    x + y
  }
}
add_4 <- add_y(4)

So the value of add_4 is a function that adds 4 to its input. This works. I would like to be use dput to show the definition of add_4 as

function(x) {
  x + 4
}

but this is not what dput returns.

add_y <- function(y) {
  function(x) {
    x + y
  }
}
add_4 <- add_y(4)
dput(add_4)
#> function (x) 
#> {
#>     x + y
#> }

Is there a way to obtain source code that would run outside of the enclosing environment?

4

There are 4 best solutions below

5
G. Grothendieck On BEST ANSWER

If you control add_y then a workaround would be either to inject the value of y right into the body of the inner function or else inject it into the formal argument list. That eliminates the use of environments so the problem no longer exists. This involves naming the anonymous inner function and only one extra line to perform the injection plus one line to return the result.

# 1. inject into body
add_y2 <- function(y) {
  inner <- function(x) {
    x + y
  }
  body(inner) <- do.call("substitute", list(body(inner)))
  inner
}
# test
add_4 <- add_y2(4)
dput(add_4)
## function (x) 
## {
##     x + 4
## }

# 2. inject into formal arguments
add_y3 <- function(y) {
  inner <- function(x) {
    x + y
  }
  formals(inner)$y <- y
  inner
}
# test
add_4 <- add_y3(4)
dput(add_4)
## function (x, y = 4) 
## {
##     x + y
## }
0
MrFlick On

Not with a dput(), no. The dput() function will not create text representations of environments.

If you want to save the function, you could do

save(add_4, file="add4.Rdata")

and then in another R session

load("add4.Rdata")

That will capture all the enclosed values and your function will behave as it did before

0
jpdugo17 On

This can work but it involves changing the contents of add_y.

library(rlang)
library(magrittr)
library(stringr)

add_y <- function(y) {
  fn <- expr(function(x) {
    x+!!y
  })
  fn <- deparse(fn) %>% str_c(collapse = "")
  fn <- eval(parse(text = fn))
}

add_4 <- add_y(4)

dput(add_4)
#> function (x) 
#> {
#>     x + 4
#> }

Created on 2021-12-24 by the reprex package (v2.0.1)

0
user2554330 On

You could construct a dput replacement that generated code that creates a function like add_4, but it wouldn't deparse the way you want:

dput_with_env <- function(f) {
  fn <- deparse(f, control = c("keepNA", "keepInteger", 
                               "niceNames", "showAttributes"))
  env <- as.list(environment(f))
  cat("local({ f =\n")
  cat(fn, sep = "\n")
  cat("\nenvironment(f) <- list2env(\n")
  dput(env)
  cat(")\nf})")
}

add_y <- function(y) {
  function(x) {
    x + y
  }
}
add_4 <- add_y(4)

dput_with_env(add_4)
#> local({ f =
#> function (x) 
#> {
#>     x + y
#> }
#> 
#> environment(f) <- list2env(
#> list(y = 4)
#> )
#> f})

Created on 2021-12-24 by the reprex package (v2.0.1)

This assumes that the environment of add_4 is quite simple, so the parent of its environment can be the environment in place when you evaluate the code. We can try it out:

newfn <- local({ f =
function (x) 
{
   x + y
}
environment(f) <- list2env(
list(y = 4)
)
f})

newfn
#> function (x) 
#> {
#>    x + y
#> }
#> <environment: 0x7f9a1b5e2318>
newfn(1)
#> [1] 5

Created on 2021-12-24 by the reprex package (v2.0.1)