1

I'd like to have a function to paste logical expressions

paste_logic(a == b, c > q, f < g, sep = and) 
# should return
# expr(a == b & c > q & f < g)

I would also like to lazily unquote during ruturning (not in the function call), ideally control which side

paste_expr(paste_expr(a == b, c > q, f < g, sep = and, side = right)
# should return
# expr(a == !!b & c > !!q & f < !!g)

The solution I got towards the first goal is:

paste_logic <- function(sep, ...) {
  dots <- enquos(...)
  sep <- enexpr(sep)
  dispatch <- function(symbol) if (symbol == expr(and)) `&` else `|`
  if (length(dots) == 1) {
    dots[[1]]
  } else {
    expr(`!!`(dispatch(sep))(!!(dots[[1]]), !!paste_logic(sep, !!!dots[-1])))
  }
}
paste_logic(and, a > b, c == d, k == f) 
# returns
# .Primitive("&")(~a > b, .Primitive("&")(~c == d, ~k == f))

and

a <- 1
b <- 2
c <- 3
d <- 3
k <- 9
f <- 10

eval_tidy(paste_logic(and, a > b, c == d, k == f))
# returns FALSE
eval_tidy(paste_logic(or, a > b, c == d, k == f))
# returns TRUE

Both are as expected.

I have a few questions on how to improve this piece of code and achieve the 2nd goal (unquoting by side in returned expression).

Q1. In this part in the last else {...} closure:

expr(`!!`(dispatch(sep))(!!(dots[[1]]), !!paste_logic(sep, !!!dots[-1])))

I have to use prime signs to wrap the !! operator or use UQ function. If I simply give it as !!(dispatch(sep)) or with full function definition as this

paste_logic <- function(sep, ...) {
  dots <- enquos(...)
  sep <- enexpr(sep)
  dispatch <- function(symbol) if (symbol == expr(and)) `&` else `|`
  if (length(dots) == 1) {
    dots[[1]]
  } else {
    expr(!!(dispatch(sep))(!!(dots[[1]]), !!paste_logic(!!sep, !!!dots[-1])))
  }
}
paste_logic(or, a > b, c == d, k == f)

It returns error

Error: Quosures can only be unquoted within a quasiquotation context.

  # Bad:
  list(!!myquosure)

  # Good:
  dplyr::mutate(data, !!myquosure)

Testing in global environment

a <- 1
b <- 2
c <- `&`
expr(!!(c)(!!a, !!n))

works fine without error and returns TRUE. So, why in my code this does not work and I have to use <prime>!!<prime>?

Q2. I have to use the prefix functional version of the logical operators, thus the final expression is recursive function calls to .Primitive("&").

Is there a way to pass & and | as symbols from outside of the function so I get final expression as expr(a == b & c > q & f < g)?

Simply wrapping & and | with ensym or enexpr inside function body generates errors like: Error: unexpected '&' in "expr(&"

Q3. This solution does not support further unquoting within the returned expression such as

expr(a == !!b & c > !!q & f < !!g)

since each dots[[i]] is a single expression like a == b which I couldn't further decompose and manipulate with. Defining side to be unquoted is even harder to accomplish. Is there any simple way to achieve this?

englealuze
  • 1,445
  • 12
  • 19
  • Could you post a summary of the problem you're actually trying to solve? I'm not convinced you're going about it the right way. – wurli Oct 26 '21 at 10:10
  • @wurli, the problem I am going to solve is self-explained by the function I want to write: I want to paste several single logical expressions to a complete logical expression using a functional way. I am not working with any data objects. I am working with code directly. – englealuze Oct 26 '21 at 10:19
  • Sorry, I was unclear. What would you use such a function for? Or are you just trying to learn more about meta-programming in R? – wurli Oct 26 '21 at 10:22
  • 1
    One practical example for this pattern is `dplyr::filter()` which internally joins inputs in `...` with boolean operators. – Lionel Henry Oct 26 '21 at 10:23
  • 1
    @wurli Thank you for the interest. I am using this to learn the fine details of metaprogramming and rlang in R. I do have an use case for building a simple language interpreter. There is a piece related to logics. To work on that I need to first understand R's supports on manipulating and evaluating expressions with concerns of scope and environmental safety as much as I can. You can think this is a toy example. I have other toy examples on the way. – englealuze Oct 26 '21 at 10:29

2 Answers2

2

I think you're looking for a reducing operation:

exprs_reduce <- function(xs, op) {
  n <- length(xs)

  if (n == 0) {
    return(NULL)
  }

  if (n == 1) {
    return(xs[[1]])
  }

  # Replace `call()` by `call2()` to support inlined functions
  purrr::reduce(xs, function(out, new) call(op, out, new))
}

exprs_reduce(alist(), "&")
#> NULL

exprs_reduce(alist(foo), "&")
#> foo

exprs_reduce(alist(foo, bar), "&")
#> foo & bar

exprs_reduce(alist(foo, bar, baz), "|")
#> foo | bar | baz
Lionel Henry
  • 6,652
  • 27
  • 33
  • The list might contain quosures but then the reduced expression must be evaluated with `eval_tidy()` (in an environment where `op` is in scope, the current environment should typically work but not necessarily if `op` is supplied by the user). – Lionel Henry Oct 26 '21 at 10:44
0

I would defer to Lionel Henry's expertise here in the general approach to your problem, but to have your function do exactly what you are asking, you could try the following approach:

library(rlang)

paste_logic <- function(sep, ..., side = "none")
{
  elements <- as.list(match.call())[-1]
  sep <- elements$sep
  elements <- elements[!nzchar(names(elements))]

  sep <- if(sep == expr(and)) " & " else " | "
  if(side == "right") {
    elements <- lapply(elements, function(x) {
      x <- as.character(x); 
      x[3] <- paste0("!!", x[3]); 
      str2lang(paste(x[c(2, 1, 3)], collapse = " "))})
  }
  if(side == "left") {
    elements <- lapply(elements, function(x) {
      x <- as.character(x); 
      x[2] <- paste0("!!", x[2]); 
      str2lang(paste(x[c(2, 1, 3)], collapse = " "))})
  }
  result <- do.call(function(...) paste(..., sep = sep), 
                    lapply(elements, capture.output))
  return(str2lang(result))
}

This will return actual language objects, with the optional bang-bang operators:

paste_logic(and, a == b, c > q, f < g)
#> a == b & c > q & f < g

paste_logic(or, a == b, c > q, f < g)
#> a == b | c > q | f < g

paste_logic(and,  a == b, c > q, f < g, side = "left")
#> !!a == b & !!c > q & !!f < g

paste_logic(and,  a == b, c > q, f < g, side = "right")
#> a == !!b & c > !!q & f < !!g

And of course these can be evaluated as expected:

a <- 1
b <- 2
c <- 3
d <- 3
k <- 9
f <- 10

eval_tidy(paste_logic(and, a > b, c == d, k == f))
#> [1] FALSE

eval_tidy(paste_logic(or, a > b, c == d, k == f))
#> [1] TRUE

Created on 2021-10-26 by the reprex package (v2.0.0)

Allan Cameron
  • 147,086
  • 7
  • 49
  • 87
  • Thanks for this answer. I see you use `str2lang` . I could hardly find detailed doc for it and it's a base function. Is there any tidyeval equivalent? – englealuze Dec 12 '21 at 08:05