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?

crf
  • 1,810
  • 3
  • 15
  • 23
  • Why don't you capture the `formals` and `args` within the `local` environment? – NelsonGon Dec 24 '21 at 20:07
  • 1
    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? – Roland Dec 24 '21 at 22:19

4 Answers4

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
## }
G. Grothendieck
  • 254,981
  • 17
  • 203
  • 341
  • 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? – IceCreamToucan Dec 24 '21 at 23:01
  • That's a good idea if you don't control the function. – G. Grothendieck Dec 24 '21 at 23:13
  • 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? – crf Dec 27 '21 at 01:59
  • 1
    `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. – G. Grothendieck Dec 27 '21 at 03:52
  • 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 :) – crf Dec 27 '21 at 03:55
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)

jpdugo17
  • 6,816
  • 2
  • 11
  • 23
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)

user2554330
  • 37,248
  • 4
  • 43
  • 90
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

MrFlick
  • 195,160
  • 17
  • 277
  • 295