[Rd] Lazy-evaluate elements wrapped with invisible

Simon Urbanek @|mon@urb@nek @end|ng |rom R-project@org
Sun Oct 30 08:43:04 CET 2022


Dipterix,

I think delayedAssign() example you posted does what you want - if you don't assign the environment, it will be garbage-collected. If you fetch the value, it will be evaluated. However, I think what you meant is to have the result in one specific delayed symbol so:

a <- function() {
 v <- rnorm(1e8)
 local(delayedAssign('x', {
   list(m = mean(v), plot_data = 1:3)
 }))
}
m1 <- a()$x

If you don't assign it, everything will be garbage-collected. If you assign it, it is evaluated and you can garbage-collect the object you needed for its creation.

The only difference is that you need to refer to the value if you decide to assign it, so you never really want to use the environment itself.


With your delayed() wish you are really asking for a "naked" promise without the assignment part, but that is not allowed in R, because promises are only forced as part of symbol evaluation. If you do that (have delayed() return a promise), you will get this:

a <- function() {
 v <- rnorm(1e8)
 return(delayed({
   cat("computing...\n");
   list(m = mean(v), plot_data = 1:3)
 }))
}

> m1 <- a()
> m1
computing...
$m
[1] 0.03834447

$plot_data
[1] 1 2 3

> m2 <- { a(); a(); a() }
> m2
computing...
$m
[1] 0.05177236

$plot_data
[1] 1 2 3


But you cannot use the actual value without assigning it:

> a()$m
Error in a()$m : object of type 'promise' is not subsettable

because R will not try to evaluate a promise anywhere but in symbol evaluation.

Cheers,
Simon



> On Oct 30, 2022, at 11:57 AM, Dipterix Wang <dipterix.wang using gmail.com> wrote:
> 
> Sorry I think I intended to say that 
> 
> 1. the expressions within `delayed` don’t have to be executed if not assigned, and 
> 2. the enclosing runtime environment that is potentially referenced by the objects within delayed() can be released immediately either the returned values are referenced or not (compared to the environment() approach). 
> 
> For a toy example,
> 
> a <- function() {
>  v <- rnorm(1e8)
>  return(delayed({
>    list(m = mean(v), plot_data = 1:3)
>  }))
> }
> m1 <- a()
> 
> can immediately release the function runtime environment either the results of `a()` is assigned to an object (`mean(v)` is evaluated) or not (delayed is not evaluated, and gc’ed), hence the object `v` is freed from the memory.
> 
> Compared to the `delayedAssign+environment()` approach
> 
> b <- function(){
>  v <- rnorm(1e8)
>  delayedAssign(‘m’, mean(v))
>  plot_data <- 1:3
>  return(environment())
> }
> m2 <- b()
> 
> `v` will be kept in memory until the returned environment itself gets deleted (rm(m2)). The situation might get tricky if the result of  “b()” is further used and returned in nested pipes… (might keep parent frame, parent parent frames…)
> 
> - D
> 
>> On Oct 29, 2022, at 11:54 AM, Bill Dunlap <williamwdunlap using gmail.com> wrote:
>> 
>>> the `delayed` object is ready to be garbage collected if not assigned immediately.
>> I am not sure what is meant here.  Any object (at the R code level) is ready to be garbage collected if not given a name or is not part of an object with a name.  Do you mean a 'delayed' component of a list should be considered garbage if not 'immediately' extracted from a list?   Could you show a few usage cases?
>> 
>> -Bill
>> 
>> On Fri, Oct 28, 2022 at 7:41 PM Dipterix Wang <dipterix.wang using gmail.com <mailto:dipterix.wang using gmail.com>> wrote:
>> 
>>> This is not quite true. The value, even when invisible, is captured by .Last.value, and 
>>> 
>>>> f <- function() invisible(5)
>>>> f()
>>>> .Last.value
>>> [1] 5
>> 
>> 
>> I understand .Last.value will capture the function returns, but that only happens in the top-level... I guess?
>> 
>> In the followings code, I think .Last.value does not capture the results of f, h, k, l
>> 
>> g <- function() {
>>  f(); h(); k(); l()
>>  return()
>> }
>> g()
>> 
>> 
>> Maybe I caused confusion by mentioning `invisible` function. I guess it should be a new function (let’s call it `delayed`). The function does not have to be limited to “printing”. For example, a digest key
>> 
>> 
>> a <- function(key, value) {
>>  map$set(key, value)
>> 
>>  return(delayed({
>>    digest(value)
>>  }))
>> }
>> 
>> Or an async evaluation of which the saved result might not be needed if not assigned (detached), or the result will be “joined” to the main process
>> 
>> a <- function(path) {
>>  # async 
>>  f <- future::future({
>>    # calculate, and then write to path
>>    saveRDS(…, path)
>>  })
>> 
>>  return(delayed({
>>    resolve(f) # wait till f to finish
>> 
>>    readRDS(path)
>>  }))
>> }
>> 
>> Although I could use wrappers such as formula, quosure, or environment to achieve similar results, there are two major differences
>> 
>> 1. There is an extra call to get the lazy-evaluated results (if I do want to resolve it)
>> 2. The returned objects have to contain sort of “environment” component in it. It can’t just be simple objects like vectors, matrices, lists, … (also you can't immediately garbage collect the enclosing environment)
>> 
>> From the implementation perspective, the `delayed` object is ready to be garbage collected if not assigned immediately.
>> 
>> Best,
>> - D
>> 
>>> 
>>> This is not quite true. The value, even when invisible, is captured by .Last.value, and 
>>> 
>>>> f <- function() invisible(5)
>>>> f()
>>>> .Last.value
>>> [1] 5
>>> 
>>> Now that doesn't actually preclude what you're suggesting (just have to wait for .Last.value to be populated by something else), but it does complicate it to the extent that I'm not sure the benefit we'd get would be worth it.
>>> 
>>> Also, in the case you're describing, you'd be pushing the computational cost into printing, which, imo, is not where it should live. Printing a values generally speaking, should just print things, imo.
>>> 
>>> That said, if you really wanted to do this, you could approach the behavior you want, I believe (but again, I think this is a bad idea) by returning a custom class that wraps formula (or, I imagine, tidyverse style quosures) that reach back into the call frame you return them from, and evaluating them only on demand.
>>> 
>>> Best,
>>> ~G 
>>> 
>>> 
>>> This idea is somewhere between `delayedAssign` and eager evaluation. Maybe we could call it delayedInvisible()?
>>> 
>>> Best,
>>> - Zhengjia
>>> 
>>> ______________________________________________
>>> R-devel using r-project.org <mailto:R-devel using r-project.org> mailing list
>>> https://stat.ethz.ch/mailman/listinfo/r-devel <https://stat.ethz.ch/mailman/listinfo/r-devel>
>> 
> 
> 
> 	[[alternative HTML version deleted]]
> 
> ______________________________________________
> R-devel using r-project.org mailing list
> https://stat.ethz.ch/mailman/listinfo/r-devel
> 



More information about the R-devel mailing list