Finding rlang types of an object

As I dig into tidyeval I find that an object can have a number of types, like personalities, associated with it. These appear to be like iterfaces in C# or Objective C or a number of other languages. Sometimes I find it helps to know what those types are. For example something like

~ a + b

is looked at as being a formula, of course. But it is also looked at as being formulaish along with being a callable, copyable, quosure, quosureish, and symbolic.

I made trivial function that returns all the "types" of an object.. here is an example of it in action.

suppressPackageStartupMessages(library(tidyverse))

sybil <- environment() 
te_what_is_it(sybil) # :-) 
#> [1] "bare_env"      "dictionaryish" "env"           "named"

te_what_is_it(~ a + b)
#> [1] "bare_formula" "callable"     "copyable"     "expr"        
#> [5] "formula"      "formulaish"   "quosureish"   "symbolic"

te_what_is_it(rlang::quo(~ a + b))
#> [1] "callable"   "copyable"   "expr"       "formula"    "formulaish"
#> [6] "quosure"    "quosureish" "symbolic"

te_what_is_it(c(1, 2, 3))
#>  [1] "atomic"          "bare_atomic"     "bare_double"    
#>  [4] "bare_integerish" "bare_numeric"    "bare_vector"    
#>  [7] "copyable"        "double"          "integerish"     
#> [10] "vector"

Here is the code for it in case anyone would find it useful. It just a small function and a "big" table. It's
completely untested, of course, but if you run into an issue with it just let me know.

PS: I would have made a package out of it but I haven't quite figured out all the details of that yet...

# Dan Sullivan
# utility for finding tidy types of an object

te_what_is_it <- function(obj) {
	is_functions$title %>%
		tibble::tibble() %>%
		dplyr::filter(purrr::map_lgl(is_functions$test, ~ .(obj))) %>%
		purrr::flatten_chr()
}

# the table of the "is" functions that test for a type
is_functions <- tibble::tribble(
	~ test, ~ title,
	rlang::is_atomic, "atomic",
	rlang::is_bare_atomic, "bare_atomic",
	rlang::is_bare_bytes, "bare_bytes",
	rlang::is_bare_character, "bare_character",
	rlang::is_bare_double, "bare_double",
	rlang::is_bare_env, "bare_env",
	rlang::is_bare_formula, "bare_formula",
	rlang::is_bare_integer, "bare_integer",
	rlang::is_bare_integerish, "bare_integerish",
	rlang::is_bare_list, "bare_list",
	rlang::is_bare_logical, "bare_logical",
	rlang::is_bare_numeric, "bare_numeric",
	rlang::is_bare_raw, "bare_raw",
	rlang::is_bare_string, "bare_string",
	rlang::is_bare_vector, "bare_vector",
	rlang::is_binary_lang, "binary_lang",
	rlang::is_bytes, "bytes",
	rlang::is_call_stack, "call_stack",
	rlang::is_callable, "callable",
	rlang::is_character, "character",
	rlang::is_chr_na, "chr_na",
	rlang::is_closure, "closure",
	rlang::is_copyable, "copyable",
	rlang::is_cpl_na, "cpl_na",
	rlang::is_dbl_na, "dbl_na",
	rlang::is_definition, "definition",
	rlang::is_dictionary, "dictionary",
	rlang::is_dictionaryish, "dictionaryish",
	rlang::is_double, "double",
	rlang::is_empty, "empty",
	rlang::is_env, "env",
	rlang::is_eval_stack, "eval_stack",
	rlang::is_expr, "expr",
	rlang::is_false, "false",
	rlang::is_formula, "formula",
	rlang::is_formulaish, "formulaish",
	rlang::is_frame, "frame",
	rlang::is_function, "function",
	rlang::is_int_na, "int_na",
	rlang::is_integer, "integer",
	rlang::is_integerish, "integerish",
	rlang::is_lgl_na, "lgl_na",
	rlang::is_list, "list",
	rlang::is_logical, "logical",
	rlang::is_na, "na",
	rlang::is_named, "named",
	rlang::is_node, "node",
	rlang::is_null, "null",
	rlang::is_pairlist, "pairlist",
	rlang::is_primitive, "primitive",
	rlang::is_primitive_eager, "primitive_eager",
	rlang::is_primitive_lazy, "primitive_lazy",
	rlang::is_quosure, "quosure",
	rlang::is_quosureish, "quosureish",
	rlang::is_quosures, "quosures",
	rlang::is_raw, "raw",
	rlang::is_scalar_atomic, "scalar_atomic",
	rlang::is_scalar_bytes, "scalar_bytes",
	rlang::is_scalar_character, "scalar_character",
	rlang::is_scalar_double, "scalar_double",
	rlang::is_scalar_integer, "scalar_integer",
	rlang::is_scalar_integerish, "scalar_integerish",
	rlang::is_scalar_list, "scalar_list",
	rlang::is_scalar_logical, "scalar_logical",
	rlang::is_scalar_raw, "scalar_raw",
	rlang::is_scalar_vector, "scalar_vector",
	rlang::is_stack, "stack",
	rlang::is_string, "string",
	rlang::is_symbol, "symbol",
	rlang::is_symbolic, "symbolic",
	rlang::is_syntactic_literal, "syntactic_literal",
	rlang::is_true, "true",
	rlang::is_vector, "vector"
	)

Dan

5 Likes

This could be such a helpful utility function when teaching tidy eval!

Nice idea ! Thanks ! could be useful.

As a code golf attempt, here is a shorter version without pipes and using purrr::invoke_map for iterating through a list a function:

te_what_is_it <- function(obj) {
  is_functions$title[purrr::invoke_map_lgl(is_functions$test, x = obj)]
}
3 Likes

I always wondered who those code golfers were… :golf:

@danr any plans to write this one up? Yet again, I'm super stoked on something I see here, and I want to immediately share it unto the world!

You can write it up if you want. I will be doing something with it but not right now.

1 Like

I thought I would aim to make the is_functions tibble dynamic using getNamespaceExports, because that way we can grab new rlang::is_whatever() functions in the future. However, there are currently at least two functions, is_installed and is_scoped, that throw up errors on some inputs. Since that depends on the input, I dynamically ignore the error-causing functions using try.

suppressPackageStartupMessages(library(tidyverse))

is_functions <- tibble(
  title = 
    getNamespaceExports("rlang") %>%    # Find all rlang functions
    stringr::str_match("^is_(.+)") %>%  # Pull out the "is_" titles
    .[,2] %>%                           # Only keep matches
    discard(~ is.na(.x)) %>%            # Discard non-"is_" functions
    sort(),                             # Sort for a nice order
  test =
    title %>%
    stringr::str_c("is_", .) %>%        # Add the prefix back
    map(rlang::as_function,             # Convert strings to functions
        env = getNamespace("rlang"))        # Using the rlang environment
)

te_what_is_it <- function(obj) {
  is_functions %>%
    filter(test %>%
             # Run all of the "is_" functions
             # Since some "is_" functions may not be valid, such
             # as is_installed, silently drop those that produce errors
             map_lgl(~ try(.x(obj), silent = TRUE) %>% 
                       rlang::is_true())) %>%
    pull(title)
}

sybil <- environment() 
te_what_is_it(sybil) # :-) 
#> [1] "bare_env"      "dictionaryish" "env"           "named"

te_what_is_it(~ a + b)
#>  [1] "bare_formula" "callable"     "copyable"     "expr"        
#>  [5] "formula"      "formulaish"   "lang"         "quosureish"  
#>  [9] "symbolic"     "unary_lang"

te_what_is_it(rlang::quo(~ a + b))
#>  [1] "callable"   "copyable"   "expr"       "formula"    "formulaish"
#>  [6] "lang"       "quosure"    "quosureish" "symbolic"   "unary_lang"

te_what_is_it(c(1, 2, 3))
#>  [1] "atomic"          "bare_atomic"     "bare_double"    
#>  [4] "bare_integerish" "bare_numeric"    "bare_vector"    
#>  [7] "copyable"        "double"          "integerish"     
#> [10] "vector"

te_what_is_it("rlang")
#>  [1] "atomic"            "bare_atomic"       "bare_character"   
#>  [4] "bare_string"       "bare_vector"       "character"        
#>  [7] "copyable"          "expr"              "installed"        
#> [10] "scalar_atomic"     "scalar_character"  "scalar_vector"    
#> [13] "string"            "syntactic_literal" "vector"

It's odd to me, but the env = getNamespace("rlang") argument to rlang::as_function doesn't actually seem necessary. I included it because the fact that it worked without it didn't make sense to me.

3 Likes

This is a better way to build the is_functions table. I just wasn't familiar with all the introspection functions available in R. More digging to do :nerd_face:

Thanks @nick