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

4

8

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?

Actinomycete answered 24/12, 2021 at 19:35 Comment(2)
Why don't you capture the formals and args within the local environment?Hyperpituitarism
A closure always includes an environment. dput gives you the function but not the environment. y is stored in the environment. If you want good advice, you should explain why you need this. Why dput instead of serializing the closure?Marcus
B
7

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
## }
Baldric answered 24/12, 2021 at 21:42 Comment(5)
I don't think you need to change add_y. Shouldn't body(add_4) <- do.call(substitute, list(body(add_4), environment(add_4))) work?Baugh
That's a good idea if you don't control the function.Baldric
this works perfectly, but I am a little bit fuzzy on why! I think I need a better understanding of what "substitute" does... any advice on where I can read up on this?Actinomycete
help(substitute) would be the main authoritative source. substitute replaces each variable found in the expression passed to it with its value for variables in the environment specified as the second argument and if not specified, as here, it uses the current environment.Baldric
with respect to the authors, I found this comment significantly more readable and easy to understand than what I found in help(substitute)! Thank you :)Actinomycete
O
3

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)

Oppidan answered 24/12, 2021 at 20:27 Comment(0)
F
2

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)

Fleur answered 24/12, 2021 at 20:51 Comment(0)
P
1

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

Permanent answered 24/12, 2021 at 19:56 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.