Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use named with dotty #2

Open
JosiahParry opened this issue Sep 22, 2024 · 8 comments
Open

Use named with dotty #2

JosiahParry opened this issue Sep 22, 2024 · 8 comments

Comments

@JosiahParry
Copy link

I would really love to be able to access an object's names while destructuring. I was hoping that by using names(value) i could accomplish this due to the assignment method from dotty, but it isn't yet possible.

Here is a repro use case:

library(dotty)

env_vars <- list(
  "api_key" = "fbivyb137294hgwv",
  "hello" = "world",
  "current_user" = "[email protected]"
)
for (i in seq_along(env_vars)) {
  .[k = names(value), v] <- env_vars[i]
}

In an ideal world it could look something like

for (.[k, v] in env_vars) {
   ...
}
@kevinushey
Copy link
Owner

kevinushey commented Sep 25, 2024

I think the best solution in these scenarios is a helper like enumerate:

enumerate <- function(x, f, ..., FUN.VALUE = NULL) {

  n <- names(x)
  idx <- `names<-`(seq_along(x), n)
  callback <- function(i) f(n[[i]], x[[i]], ...)

  if (is.environment(x))
    x <- as.list(x, all.names = TRUE)

  if (is.null(FUN.VALUE))
    lapply(idx, callback)
  else
    vapply(idx, callback, FUN.VALUE = FUN.VALUE)

}

The main downside being that you lose out on the control flow options normally available within a for loop.

What would you think of an alternate syntax like the following?

dotty_enum(env_vars, .[key, value] <- {
  # do something; behavior like a for loop
})

The syntax is a little magical looking, but it's relatively lightweight, and the magic helps make it clear that this isn't a "regular" function invocation. This seems to work as expected:

env_vars <- list(
  "api_key" = "fbivyb137294hgwv",
  "hello" = "world",
  "current_user" = "[email protected]"
)

dotty_enum <- function(values, dotty) {
  
  # Make sure we received a dotty assignment call.
  dotty <- substitute(dotty)
  ok <-
    identical(dotty[[1L]], as.symbol("<-")) &&
    is.call(dotty[[2L]]) &&
    identical(dotty[[2L]][[1L]], as.symbol("[")) &&
    identical(dotty[[2L]][[2L]], as.symbol("."))
  
  if (!ok)
    stop("expected a dotty expression of the form `.[key, value] <- {}`")
    
  # Pull out the dotty variables and dotty expression.
  keyvar <- as.character(dotty[[2L]][[3L]])
  valvar <- as.character(dotty[[2L]][[4L]])
  expr <- dotty[[3L]]
  
  # Create an expression that we can evaluate in the parent frame.
  # Since we're creating variables, we'll need to be careful about
  # cleanup. We do this so that control flow constructs work as expected.
  data <- list(
    ..values.. = substitute(values),
    ..keyvar.. = keyvar,
    ..valvar.. = valvar,
    ..expr..   = expr
  )
  
  expr <- substitute(env = data, {
    
    for (..i.. in seq_along(..values..)) {
      assign(..keyvar.., names(..values..)[[..i..]])
      assign(..valvar.., ..values..[[..i..]])
      evalq(..expr..)
    }
    
  })
  
  eval(expr, envir = parent.frame())
  
}

dotty_enum(env_vars, .[key, value] <- {
  str(list(key, value))
  if (key == "hello")
    break
})

That gives the expected:

> dotty_enum(env_vars, .[key, value] <- {
+   str(list(key, value))
+   if (key == "hello")
+     break
+ })
List of 2
 $ : chr "api_key"
 $ : chr "fbivyb137294hgwv"
List of 2
 $ : chr "hello"
 $ : chr "world"

@kevinushey
Copy link
Owner

Still, the question is whether this is more useful than purrr::imap -- the main benefit with this approach is the for-loop semantics + ability to work directly in the current frame?

@JosiahParry
Copy link
Author

This is better than nothing, I agree. For one, purrr is a rather heavy dependency whereas dotty is a tiny base R dep.

There are also some cases where I feel that the ergonomics of a for loops are just better—particularly when running code for their side effects.

pkg_size_recursive("purrr")
#>          pkg    size
#> 1      total  15.27M
#> 2      vctrs    3.9M
#> 3      rlang    2.5M
#> 4        cli    2.4M
#> 5      utils    2.3M
#> 6    methods    2.1M
#> 7      purrr    836K
#> 8   magrittr    536K
#> 9  lifecycle    376K
#> 10      glue    368K
pkg_size_recursive("dotty")
#>     pkg size
#> 1 dotty  84K
#> 2 total  84K

Here is the use case that really motivated this line of enquiry

env_vars <- list(
  "api_key" = "fbivyb137294hgwv",
  "hello" = "world",
  "current_user" = "[email protected]"
)

n <- length(env_vars)
encoded_keys <- character(n)
encoded_vals <- vector("list", n)

keys <- names(env_vars)
vals <- unlist(env_vars)

for (i in 1:n) {
  k <- keys[i]
  v <- vals[i]
  # note that I do rsa encryption here in my actual use case 
  encoded_keys[i] <- b64::encode(k)
  encoded_vals[[i]] <- b64::encode(v)
}

# to put in REST request
setNames(encoded_vals, encoded_keys)

similar code in rust feels much nicer here. This is partially due to the fact that in R we can preallocate a list but if we try to assign based on a name in a preallocated list it will create a new entry.

use base64::prelude::*;

fn main() {
    let env_vars = [
        ("api_key", "fbivyb137294hgwv"),
        ("hello", "world"),
        ("current_user", "[email protected]"),
    ];

    let mut encrypted = Vec::with_capacity(env_vars.len());

    for (k, v) in env_vars {
        let ke = BASE64_STANDARD.encode(k);
        let ve = BASE64_STANDARD.encode(v);
        encrypted.push((ke, ve))
    }
}

@kevinushey
Copy link
Owner

kevinushey commented Sep 26, 2024

What do you think of the proposed syntax?

.enum(object, .(key, val) -> {
  # do stuff with key, val; for-loop semantics apply
})

@JosiahParry
Copy link
Author

I quite like it! I'm curious though, how it could also handle an enumeration with keys and index?

For example in Rust (sorry, my second more comfortable language) I might write for (i, (k, v)) in x.into_iter().enumerate()

@kevinushey
Copy link
Owner

kevinushey commented Sep 26, 2024

We could allow for an optional third parameter, e.g.

.enum(object, .(key, value, idx) -> { ... })

I'd want to make it the third parameter here just because I think key + value would almost always be useful, but idx may or may not be.

@JosiahParry
Copy link
Author

Absolutely. And would this behave similarly to imap / iwalk where if there are no name the element index is provided to key?

@kevinushey
Copy link
Owner

Yeah, I think that's sensible.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants