11 Expressions
This chapter was written and contributed by Peter Hurford.
11.1 Structure of expressions
Q: There’s no existing base function that checks if an element is a valid component of an expression (i.e., it’s a constant, name, call, or pairlist). Implement one by guessing the names of the “is” functions for calls, names, and pairlists.
A:
is.expression_component <- function(x) { x <- substitute(x) (is.atomic(x) & length(x) == 1) | is.call(x) | is.name(x) | is.pairlist(x) }
Q:
pryr::ast()
uses non-standard evaluation. What’s its escape hatch to standard evaluation?A: You can call
pryr::call_tree
directly.Q: What does the call tree of an if statement with multiple else conditions look like?
A: It depends a little bit how it is written. Here the infix version:
pryr::ast(`if`(FALSE, "first", `if`(TRUE, "second", `if`(TRUE, "third", "fourth")))) #> \- () #> \- `if #> \- FALSE #> \- "first" #> \- () #> \- `if #> \- TRUE #> \- "second" #> \- () #> \- `if #> \- TRUE #> \- "third" #> \- "fourth"
And here the “normal” version:
pryr::ast(if (FALSE) { "first" } else ( if (TRUE) { "second" } else ( if (TRUE) { "third" } else ( "fourth" ) ) )) #> \- () #> \- `if #> \- FALSE #> \- () #> \- `{ #> \- "first" #> \- () #> \- `( #> \- () #> \- `if #> \- TRUE #> \- () #> \- `{ #> \- "second" #> \- () #> \- `( #> \- () #> \- `if #> \- TRUE #> \- () #> \- `{ #> \- "third" #> \- () #> \- `( #> \- "fourth"
However, under the hood the language will call another base
if
statement. Soelse if
seems to be for human readibility.Q: Compare
ast(x + y %+% z)
toast(x ^ y %+% z)
. What do they tell you about the precedence of custom infix functions?A: Comparison of the syntax trees:
# for ast(x + y %+% z) # y %+% z will be calculated first and the result will be added to x pryr::ast(x + y %+% z) #> \- () #> \- `+ #> \- `x #> \- () #> \- `%+% #> \- `y #> \- `z # for ast(x ^ y %+% z) # x^y will be calculated first, and the result will be used as first argument of `%+%()` pryr::ast(x ^ y %+% z) #> \- () #> \- `%+% #> \- () #> \- `^ #> \- `x #> \- `y #> \- `z
So we can conclude that custom infix functions must have a precedence between addition and exponentiation. The general precedence rules can be found for example here.
Q: Why can’t an expression contain an atomic vector of length greater than one? Which one of the six types of atomic vector can’t appear in an expression? Why?
A: Because you can’t type an expression that evaluates to an atomic of greater length than one without using a function (in particular, the function
c
), which means that these expressions would be calls.We can illustrate that via an example:
is.atomic(quote(1)) # is an atomic #> [1] TRUE is.atomic(quote(c(1,1))) # is not an atomic (it just would evaluate to an atomic). #> [1] FALSE is.call(quote(c(1,1))) # however it is still a call (so at least a valid expression). #> [1] TRUE
Also raws can’t appear in expressions, because of a similar reason. We think they are impossible to construct without using
as.raw
, which would mean that we will also end up with a call.For similar reasons also complex numbers won’t work
(function(x){is.atomic(x) & length(x) == 1})(quote(1 + 1.5i)) #> [1] FALSE # however, imaginary parts of complex numbers work: pryr::ast(1i) #> \- 0+1i
11.2 Names
Q: You can use
formals()
to both get and set the arguments of a function. Useformals()
to modify the following function so that the default value ofx
is missing andy
is 10.g <- function(x = 20, y) { x + y }
A:
formals(g) <- alist(x = , y = 10)
Similarly one can change the body of the function through
body<-()
and also the environment viaenvironment<-()
.Q: Write an equivalent to
get()
usingas.name()
andeval()
. Write an equivalent toassign()
usingas.name()
,substitute()
, andeval()
. (Don’t worry about the multiple ways of choosing an environment; assume that the user supplies it explicitly.)A:
get3 <- function(x, env = parent.frame()) { eval(as.name(x), env) } assign3 <- function(x, value, env = parent.frame()) { eval(substitute(x <- value,list(x = as.name(x), value = value)), env) if (length(x) > 1) { warning("Only the first element is used as variable name.") } }
11.3 Calls
Q: The following two calls look the same, but are actually different:
(a <- call("mean", 1:10)) #> mean(1:10) (b <- call("mean", quote(1:10))) #> mean(1:10) identical(a, b) #> [1] FALSE
What’s the difference? Which one should you prefer?
A:
call
evalulates its...
arguments. So in the first call1:10
will be evaluated to an integer (1, 2, 3, …, 10) and in the second callquote()
compensates the effect of the evaluation, so thatb
’s second element will be the expression1:10
(which is again a call):as.list(a) #> [[1]] #> mean #> #> [[2]] #> [1] 1 2 3 4 5 6 7 8 9 10 as.list(b) #> [[1]] #> mean #> #> [[2]] #> 1:10
We can create an example, where we can see the consequences directly:
arg <- seq(10) call1 <- call("mean", arg) print(call1) #> mean(1:10) call2 <- call("mean", quote(arg)) print(call2) #> mean(arg) eval(call1) #> [1] 5.5 eval(call2) #> [1] 5.5
I would prefer the second version, since it behaves more like lazy evaluation. It’s better to have call args depends on the calling environment rather than the enclosing environment,that’s more similar to normal function behavior.
Q: Implement a pure R version of
do.call()
.A:
do.call2 <- function(what, args, quote = FALSE, env = parent.frame()) { if (!is.list(args)) { stop("second argument must be a list") } if (isTRUE(quote)) { args <- lapply(args, enquote) } eval(as.call(c(what, args)), env) }
Q: Concatenating a call and an expression with
c()
creates a list. Implementconcat()
so that the following code works to combine a call and an additional argument.concat(quote(f), a = 1, b = quote(mean(a))) #> f(a = 1, b = mean(a))
A:
concat <- function(f, ...) { as.call(c(f, list(...))) } concat(quote(f), a = 1, b = quote(mean(a))) #> f(a = 1, b = mean(a))
Q: Since
list()
s don’t belong in expressions, we could create a more convenient call constructor that automatically combines lists into the arguments. Implementmake_call()
so that the following code works.make_call(quote(mean), list(quote(x), na.rm = TRUE)) #> mean(x, na.rm = TRUE) make_call(quote(mean), quote(x), na.rm = TRUE) #> mean(x, na.rm = TRUE)
A:
make_call <- function(x, ...) { as.call(c(x, ...)) } make_call(quote(mean), list(quote(x), na.rm = TRUE)) #> mean(x, na.rm = TRUE) make_call(quote(mean), quote(x), na.rm = TRUE) #> mean(x, na.rm = TRUE)
Q: How does
mode<-
work? How does it usecall()
?A: We can explain it best, when we comment the source code:
function (x) { # when x is an expression, mode(x) will return "expression" if (is.expression(x)) return("expression") # when x is a call (or language, which is exactly the same), the first element # of the call will be coerced to character. # If the call is an autoprinting (like in quote((1))), mode will return "(". # For any other call, mode will return "call" if (is.call(x)) return(switch(deparse(x[[1L]])[1L], `(` = "(", "call")) # if x is a name (or a symbol, which is exactly the same), then mode will return "name" if (is.name(x)) "name" # otherwise, mode will return dependent on typeof(x). If typeof(x) is double or integer, # mode will return "numeric". If typeof(x) is closure, builtin or special, mode(x) will # return "function". And in all other cases, mode will just return typeof(x) else switch(tx <- typeof(x), double = , integer = "numeric", closure = , builtin = , special = "function", tx) } <bytecode: 0x000000000c4e66e0> <environment: namespace:base>
As commented above,
mode()
usesis.call()
to distinguish autoprint- and “normal” calls with the help of a separateswitch()
.Q: Read the source for
pryr::standardise_call()
. How does it work? Why isis.primitive()
needed?A: It evaluates the first element of the call, which is usually the name of a function, but can also be another call. Then is uses
match.call()
to get the standard names for all the arguments.is.primitive()
is used as an escape to just return the call instead of usingmatch.call()
if the function passed is a primitive. This is done becausematch.call()
does not work for primitives.Q:
standardise_call()
doesn’t work so well for the following calls. Why?library(pryr) #> #> Attaching package: 'pryr' #> The following object is masked _by_ '.GlobalEnv': #> #> make_call standardise_call(quote(mean(1:10, na.rm = TRUE))) #> mean(x = 1:10, na.rm = TRUE) standardise_call(quote(mean(n = T, 1:10))) #> mean(x = 1:10, n = T) standardise_call(quote(mean(x = 1:10, , TRUE))) #> mean(x = 1:10, , TRUE)
A: The reason these don’t work is not that
mean
is a primitive (as seen in exercise 6) – it’s not – but becausemean
uses S3 dispatch (i.e.,UseMethod
) and therefore does not store its formals onmean
, but rathermean.default
. For example,pryr::standardize_call
can do much better when the S3 dispatch is explicit.For example, this works:
pryr::standardise_call(quote(mean.default(n = T, 1:10))) #> mean.default(x = 1:10, na.rm = T)
Q: Read the documentation for
pryr::modify_call()
. How do you think it works? Read the source code.A: Again, we explain by commenting the source.
function (call, new_args) { # check if call is a call and new_args is a list stopifnot(is.call(call), is.list(new_args)) # standardise the call call <- standardise_call(call) # check if the supplied new_args list has any unnamed elements. # if so, an error occurs. nms <- names(new_args) %||% rep("", length(new_args)) if (any(nms == "")) { stop("All new arguments must be named", call. = FALSE) } # every name element of the call, for which a new argument was supplied by the user, # becomes overwritten for (nm in nms) { call[[nm]] <- new_args[[nm]] } # finally the modified call is returned call } <environment: namespace:pryr>
Q: Use
ast()
and experimentation to figure out the three arguments in anif()
call. Which components are required? What are the arguments to thefor()
andwhile()
calls?A:
if:
## All these return an error # pryr::ast(if) # pryr::ast(if()) # pryr::ast(if{}) # pryr::ast(if(){}) # pryr::ast(if(TRUE)) ## This is the minimum required pryr::ast(if(TRUE){1}) #> \- () #> \- `if #> \- TRUE #> \- () #> \- `{ #> \- 1 ## One can also supply an alternative expression pryr::ast(if(TRUE){} else {3}) #> \- () #> \- `if #> \- TRUE #> \- () #> \- `{ #> \- () #> \- `{ #> \- 3 ## However, one has to supply the compound expression (the first one), ## otherwise we get an error # pryr::ast(if(TRUE) # else {3}) # So this is how if basically works pryr::ast(if(cond)expr) #> \- () #> \- `if #> \- `cond #> \- `expr # and here within a call eval(call("if", TRUE, 1)) #> [1] 1 eval(call("if", TRUE, 1, 2)) #> [1] 1 eval(call("if", FALSE, 1, 2)) #> [1] 2
for:
## All these return an error # pryr::ast(for) # pryr::ast(for{}) # pryr::ast(for()) # pryr::ast(for(){}) # pryr::ast(for(in){}) # pryr::ast(for(var in){}) # pryr::ast(for(var in 10)) # pryr::ast(for(in 10){}) ## This is the minimum required pryr::ast(for(var in 10){}) #> \- () #> \- `for #> \- `var #> \- 10 #> \- () #> \- `{ ## So this is how for basically works pryr::ast(for(var in seq)expr) #> \- () #> \- `for #> \- `var #> \- `seq #> \- `expr ## And here within a call (note that we need quote, since var has to be a nanme ## and expr has to be an expression) eval(call("for", var = quote(i), seq = 1:3, expr = quote(print(i)))) #> [1] 1 #> [1] 2 #> [1] 3 ## as infix function it looks a little bit easier `for`(i, 1:3, print(i)) #> [1] 1 #> [1] 2 #> [1] 3
while:
## All these return an error # pryr::ast(while) # pryr::ast(while()) # pryr::ast(while(TRUE)) # pryr::ast(while(){}) # pryr::ast(while()1) # pryr::ast(while(TRUE){}) ## This is the minimum required pryr::ast(while(TRUE)1) #> \- () #> \- `while #> \- TRUE #> \- 1 ## So this is how while basically works pryr::ast(while(cond)expr) #> \- () #> \- `while #> \- `cond #> \- `expr ## And here within a call (infinite loop in this case) # eval(call("while", TRUE , 1))
11.4 Capturing the current call
Q: Compare and contrast
update_model()
withupdate.default()
.A:
update_call <- function (object, formula_, ...) { call <- object$call if (!missing(formula_)) { call$formula <- update.formula(formula(object), formula_) } pryr::modify_call(call, pryr::dots(...)) } update_model <- function(object, formula_, ...) { call <- update_call(object, formula_, ...) eval(call, environment(formula(object))) }
update_model
always evaluates the resulting call, whereasupdate.default
can return the call without evaluation ifevaluate = FALSE
.update.default
evaluates the call in the environment whereupdate.default
was called, whereasupdate_model
evaluates the call within the environment of the object passed.update.default
handles some extras whereasupdate_model
does not.update_model
’s syntax is less verbose and easier to read.Q: Why doesn’t
write.csv(mtcars, "mtcars.csv", row = FALSE)
work? What property of argument matching has the original author forgotten?A:
write.csv
rewrites the call. While doing this, the author explicitly matches the argument names, forgetting that this is too strict, since R does also partial matching.Q: Rewrite
update.formula()
to use R code instead of C code.A:
modify_formula <- function(old, new) { old_lhs <- old[[2]] old_rhs <- old[[3]] if (length(new) == 2) { new_lhs <- old_lhs new_rhs <- new[[2]] } else { new_lhs <- new[[2]] new_rhs <- new[[3]] } new_lhs_ <- gsub(".", deparse(old_lhs), deparse(new_lhs), fixed = TRUE) new_rhs_ <- gsub(".", deparse(old_rhs), deparse(new_rhs), fixed = TRUE) as.formula(paste(new_lhs_, "~", new_rhs_)) } modify_formula(y ~ x, ~ . + x2) #> y ~ x + x2 #> <environment: 0x55876b15d168> update.formula(y ~ x, ~ . + x2) #> y ~ x + x2 update.formula(y ~ x, log(.) ~ .) #> log(y) ~ x modify_formula(y ~ x, log(.) ~ .) #> log(y) ~ x #> <environment: 0x55876b2f6b88> update.formula(. ~ u+v, res ~ .) #> res ~ u + v modify_formula(. ~ u+v, res ~ .) #> res ~ u + v #> <environment: 0x55876b889638>
Q: Sometimes it’s necessary to uncover the function that called the function that called the current function (i.e., the grandparent, not the parent). How can you use
sys.call()
ormatch.call()
to find this function?A: You can use
sys.call(-2)
.child_fn <- function() { message("I am the child_fn") message("My parent is ", sys.call(-1)) message("My grandparent is ", sys.call(-2)) } daddy_fn <- function() { child_fn() } grandpa_fn <- function() { daddy_fn() } grandpa_fn() #> I am the child_fn #> My parent is daddy_fn #> My grandparent is grandpa_fn
11.5 Pairlists
Q: How are
alist(a)
andalist(a = )
different? Think about both the input and the output.A:
alist(a)
returns an unnamed list of length 1 with the first element being the namea
(note that this refers to thename
class, which is distinct from being nameda
), so it is unsuitable for use in a function.alist(a = )
returns a named list with the first element having namea
and the first element being empty.Q: Read the documentation and source code for
pryr::partial()
. What does it do? How does it work? Read the documentation and source code forpryr::unenclose()
. What does it do and how does it work?A:
pryr::partial
takes a function and arguments and then constructs a call of that function with those args.pryr::unenclose
takes a closure and substitutes the variables in that closure for its values found in its environment, which results in the explicit function.Q: The actual implementation of
curve()
looks more likecurve3 <- function(expr, xlim = c(0, 1), n = 100, env = parent.frame()) { env2 <- new.env(parent = env) env2$x <- seq(xlim[1], xlim[2], length = n) y <- eval(substitute(expr), env2) plot(env2$x, y, type = "l", ylab = deparse(substitute(expr))) }
curve2 <- function(expr, xlim = c(0, 1), n = 100, env = parent.frame()) { f <- pryr::make_function(alist(x = ), substitute(expr), env) x <- seq(xlim[1], xlim[2], length = n) y <- f(x) plot(x, y, type = "l", ylab = deparse(substitute(expr))) }
How does this approach differ from
curve2()
defined above?A:
curve2
uses pryr::make_function instead of creating an env and evaluating within it.
11.6 Parsing and deparsing
Q: What are the differences between
quote()
andexpression()
?A: The main difference is that an expression object returned by
expression
is a list of expressions, whereas a quote is a single expression. See:as.list(expression( 2*3)) #> [[1]] #> 2 * 3 as.list(quote(2*3)) #> [[1]] #> `*` #> #> [[2]] #> [1] 2 #> #> [[3]] #> [1] 3
Q: Read the help for
deparse()
and construct a call thatdeparse()
andparse()
do not operate symmetrically on.A:
parse
anddeparse
handle length > 1 vectors differently.parse(text = c(1, 2, 3)) #> expression(1, 2, 3) deparse(c(1, 2, 3)) #> [1] "c(1, 2, 3)"
Q: Compare and contrast
source()
andsys.source()
.A:
source
is a standardGeneric created from the base package, whereassys.source
is a function exported from base.source
has many more options thansys.source
.source
can accept data from connections other than files, whereassys.source
cannot.sys.source
which is a streamlined version to source a file into an environment.Q: Modify
simple_source()
so it returns the result of every expression, not just the last one.A:
simple_source <- function(file, envir = new.env()) { stopifnot(file.exists(file)) stopifnot(is.environment(envir)) lines <- readLines(file, warn = FALSE) exprs <- parse(text = lines) if (length(exprs) == 0L) return(invisible()) invisible(lapply(exprs, function(expr) { eval(expr, envir) })) }
Q: The code generated by
simple_source()
lacks source references. Read the source code forsys.source()
and the help forsrcfilecopy()
, then modifysimple_source()
to preserve source references. You can test your code by sourcing a function that contains a comment. If successful, when you look at the function, you’ll see the comment and not just the source code.A:
simple_source <- function(file, envir = new.env()) { stopifnot(file.exists(file)) stopifnot(is.environment(envir)) lines <- readLines(file, warn = FALSE) srcfile <- srcfilecopy(file, lines, file.mtime(file), isFile = TRUE) exprs <- parse(text = lines, srcfile = srcfile, keep.source = TRUE) if (length(exprs) == 0L) return(invisible()) invisible(lapply(exprs, function(expr) { eval(expr, envir) })) }
11.7 Walking the AST with recursive functions
Q: Why does
logical_abbr()
use a for loop instead of a functional likelapply()
?A: The loop performs better because it allows for early returns. The
return(TRUE)
in the loop withinlogical_abbr()
allows the loop to return at the first sign ofTRUE
, rather than executing the entire loop, and this saves a lot of time. Also, the loop seems a lot more readible.Q:
logical_abbr()
works when given quoted objects, but doesn’t work when given an existing function, as in the example below. Why not? How could you modifylogical_abbr()
to work with functions? Think about what components make up a function.A:
logical_abbr <- function(x) { if (is.atomic(x)) { FALSE } else if (is.name(x)) { identical(x, quote(T)) || identical(x, quote(F)) } else if (is.call(x) || is.pairlist(x)) { for (i in seq_along(x)) { if (logical_abbr(x[[i]])) return(TRUE) } FALSE } else if (is.function(x)) { # Add this to handle functions. logical_abbr(body(x)) || logical_abbr(formals(x)) } else { stop("Don't know how to handle type ", typeof(x), call. = FALSE) } } f <- function(x = TRUE) { g(x + T) } logical_abbr(f) #> [1] TRUE
Q: Write a function called
ast_type()
that returns either “constant”, “name”, “call”, or “pairlist”. Rewritelogical_abbr()
,find_assign()
, andbquote2()
to use this function withswitch()
instead of nested if statements.A:
ast_type <- function(x) { if (is.atomic(x)) return("constant") if (is.name(x)) return("name") if (is.call(x)) return("call") if (is.pairlist(x)) return("pairlist") "Other" } logical_abbr <- function(x) { switch(ast_type(x), "constant" = FALSE, "name" = logical_abbr.name(x), "call" = logical_abbr.call(x), "pairlist" = logical_abbr.call(x) ) } logical_abbr.name <- function(x) { identical(x, quote(T)) || identical(x, quote(F)) } logical_abbr.call <- function(x) { for (i in seq_along(x)) { if (logical_abbr(x[[i]])) return(TRUE) } FALSE } find_assign <- function(x) { switch(ast_type(x), "constant" = character(), "name" = character(), "call" = find_assign.call(x), "pairlist" = find_assign.pairlist(x) ) } find_assign.call <- function(x) { lhs <- if (identical(x[[1]], quote(`<-`)) && is.name(x[[2]])) { as.character(x[[2]]) } else { character() } unique(c(lhs, unlist(lapply(x, find_assign)))) } find_assign.pairlist <- function(x) { unique(unlist(lapply(x, find_assign))) } bquote2 <- function(x, where = parent.frame()) { switch(ast_type(x), "constant" = x, "name" = x, "call" = bquote2.call(x, where), "pairlist" = bquote2.pairlist(x, where) ) } bquote2.call <- function(x, where) { if (identical(x[[1]], quote(.))) { # Call to .(), so evaluate eval(x[[2]], where) } else { # Otherwise apply recursively, turning result back into call as.call(lapply(x, bquote2, where = where)) } } bquote2.pairlist <- function(x, where) { as.pairlist(lapply(x, bquote2, where = where)) }
Q: Write a function that extracts all calls to a function. Compare your function to
pryr::fun_calls()
.A:
get_calls <- function(expr) { to_call <- function(expr) { Filter(is.call, as.list(expr)) } unique(as.character(unlist(lapply(expr, function(x) { if (length(to_call(x)) > 0) { get_calls(x) } else if (is.call(x) || is.name(x)) { x } })))) }
While both `pryr::fun_calls` and `get_calls` are recursive, `get_calls` is loop-based. This is potentially less efficient. Notably, `pryr::fun_calls` executes about 6x faster than `get_calls`. `get_calls` has the ability to extract calls from the formals of a function, whereas `pryr::fun_calls` cannot do that. `get_calls` can return an entire call (i.e., "get_calls(x)") whereas `pryr::fun_calls` can only return the call name. However, `get_calls` sometimes accidentally returns variable names, whereas `pryr::fun_calls` does not make this mistake. `pryr::fun_calls` is also more readable.
Q: Write a wrapper around
bquote2()
that does non-standard evaluation so that you don’t need to explicitlyquote()
the input.A:
bquote2 <- function (x, where = parent.frame()) { bquote_fn <- function(x, where) { if (is.atomic(x) || is.name(x)) {x} else if (is.call(x)) { if (identical(x[[1]], quote(.))) { eval(x[[2]], where) } else { as.call(lapply(x, bquote_fn, where = where)) } } else if (is.pairlist(x)) { as.pairlist(lapply(x, bquote_fn, where = where)) } else { stop("Don't know how to handle type ", typeof(x), call. = FALSE) } } bquote_fn(substitute(x), where) } bquote2(x == .(x))
Q: Compare
bquote2()
tobquote()
. There is a subtle bug inbquote()
: it won’t replace calls to functions with no arguments. Why?bquote(.(x)(), list(x = quote(f))) #> .(x)() bquote(.(x)(1), list(x = quote(f))) #> f(1)
A:
Here's the source for `bquote` (from `base`):
bquote <- function(expr, where = parent.frame()) { unquote <- function(e) { if (is.pairlist(e)) { as.pairlist(lapply(e, unquote)) } else if (length(e) <= 1L) { e } else if (e[[1L]] == as.name(".")) { eval(e[[2]], where) } else { as.call(lapply(e, unquote)) } } unquote(substitute(expr)) }
The subtle bug is on the line `else if (length(e) <= 1L) { e }`, where it returns `e` if `length(e)` is <= 1. `length(substitute(.(x)()))` is 1, so it will just be returned instead of parsed.
Q: Improve the base
recurse_call()
template to also work with lists of functions and expressions (e.g., as fromparse(path_to_file))
.A:
recurse_call <- function(x) { if (is.atomic(x)) { # Return a value } else if (is.name(x)) { # Return a value } else if (is.expression(x)) { # Expansion goes here to handle expressions! # Return a value } else if (is.call(x)) { # Call recurse_call recursively } else if (is.pairlist(x)) { # Call recurse_call recursively } else if (is.list(x)) { # Expansion goes here to handle lists! # Call recurse_call recursively } else { stop("Don't know how to handle type ", typeof(x), call. = FALSE) } }
For example, we can extend
logical_abbr
as follows:logical_abbr <- function(x) { if (is.atomic(x)) { FALSE } else if (is.name(x)) { identical(x, quote(T)) || identical(x, quote(F)) } else if (is.expression(x)) { # Added this part to `logical_abbr` to handle expressions. identical(x[[1]], quote(T)) || identical(x[[1]], quote(F)) } else if (is.call(x) || is.pairlist(x)) { for (i in seq_along(x)) { if (logical_abbr(x[[i]])) return(TRUE) } FALSE } else if (is.list(x)) { # Added this part to `logical_abbr` handle lists. lapply(x, logical_abbr) } else { stop("Don't know how to handle type ", typeof(x), call. = FALSE) } } logical_abbr(quote(TRUE)) #> [1] FALSE logical_abbr(quote(T)) #> [1] TRUE logical_abbr(expression(TRUE)) #> [1] FALSE logical_abbr(expression(T)) #> [1] TRUE logical_abbr(list(quote(T), quote(F), quote(TRUE))) #> [[1]] #> [1] TRUE #> #> [[2]] #> [1] TRUE #> #> [[3]] #> [1] FALSE