Protect yourself from equals assignment!


March 13, 2021

A man sits at a table with a banner that has on it R's left assignment operator (<-) and underneath it says 'change my mind'.


I present you a function that warns if an R script contains The Assignment Operator That Shall Not Be Named.

Assign of the times

So, it’s been confirmed with extremely robust and objective evidence: the left-assignment arrow (x <- 1) is better than equals (x = 1) for assignment in R.1

So, unless you hate democracy, you should protect yourself from aberrant code that uses the cursed symbol.

But what if a nefarious colleague still sends you their scuffed code?

Assignment refinement

I’ve created the appraise_assignment() function that will peek at a suspect script and warn you if it contains the foul mark.

appraise_assignment <- function(file, destroy = FALSE) {
  tokens <- getParseData(parse(file))[["token"]]
  if (any(tokens == "EQ_ASSIGN")) {            # if '='
    warning("\nme = 'disgusted'") 
    if (destroy == TRUE) {
      answer <- readline("Destroy file? y/n: ")
      if (answer == "y") cat("Have mercy! This time...")
  } else if (any(tokens == "RIGHT_ASSIGN")) {  # if '<-'
    cat("'unorthodox' -> you\n")
  } else if (any(tokens == "LEFT_ASSIGN")) {   # if '->'
    cat("you <- 'hero'\n")
  } else {

Basically, we parse() an input file and then the function uses getParseData() to extract ‘tokens’ (i.e. maths symbols, special operators, variables, etc) from the R expressions within.

In particular, it spots the token called EQ_ASSIGN, which is when = is used in the context of assignment.

I saw the assign

For demonstration purposes, I’ve written four temporary files containing left assign (<-), right assign (->), equals (=), and no assignment at all.2 Our function will catch even a single deviation in a given file.

temp <- tempdir()  # temp location to store files

  c("x <- 1", "x <- 1; y -> 1", "x <- 1; y = 1", "x"),
  c("left", "right", "equals", "none"),
  ~writeLines(.x, file.path(temp, paste0(.y, ".R")))

list.files(temp, pattern = ".R$")
[1] "equals.R" "left.R"   "none.R"   "right.R" 

First, let’s pass the file containing the unquestionably correct assignment operator.

appraise_assignment(file.path(temp, "left.R"))
you <- 'hero'

Right-assignment is left-assignment’s less-handsome sibling.

appraise_assignment(file.path(temp, "right.R"))
'unorthodox' -> you

Hold steady…

appraise_assignment(file.path(temp, "equals.R"))
Warning in appraise_assignment(file.path(temp, "equals.R")): 
me = 'disgusted'

Phew, we got a warning, so we know the file is dangerous and should never be opened.

In fact, if you set the argument destroy = TRUE in appraise_assignment(), you’ll be prompted to irrecoverably annihilate the rotten file forever.3

For completeness, is it really an R script if it doesn’t contain any assignment at all?

appraise_assignment(file.path(temp, "none.R"))

Assigning off

In conclusion, some assignment operators were created more equal than others. See Colin Fay’s round-up to learn more about the history and plethora of these symbols (and be happy that the underscore is no longer legitimate).

Anyway, welcome to the best timeline, where we all recognise <- unequivocally as the champion and = can get absolutely rekt.

If I had one wish though, it would be to make the left-assign arrow even more powerful. How about making it really long? 23 hyphens seems sufficiently dominant.

x <----------------------- 1
[1] 1

It’s a really long arrow, so I call it ‘the spear’.4 I look forward to its adoption by R Core.


Session info
Last rendered: 2024-07-14 20:15:17 BST
R version 4.4.0 (2024-04-24)
Platform: aarch64-apple-darwin20
Running under: macOS Ventura 13.2.1

Matrix products: default
BLAS:   /Library/Frameworks/R.framework/Versions/4.4-arm64/Resources/lib/libRblas.0.dylib 
LAPACK: /Library/Frameworks/R.framework/Versions/4.4-arm64/Resources/lib/libRlapack.dylib;  LAPACK version 3.12.0

[1] en_US.UTF-8/en_US.UTF-8/en_US.UTF-8/C/en_US.UTF-8/en_US.UTF-8

time zone: Europe/London
tzcode source: internal

attached base packages:
[1] stats     graphics  grDevices utils     datasets  methods   base     

loaded via a namespace (and not attached):
 [1] crayon_1.5.3.9000  vctrs_0.6.5        cli_3.6.3.9000     knitr_1.46        
 [5] rlang_1.1.4.9000   xfun_0.43          rex_1.2.1          processx_3.8.4    
 [9] purrr_1.0.2        xmlparsedata_1.0.5 data.table_1.15.4  jsonlite_1.8.8    
[13] glue_1.7.0         backports_1.4.1    htmltools_0.5.8.1  ps_1.7.6          
[17] fansi_1.0.6        rmarkdown_2.26     tibble_3.2.1       evaluate_0.23     
[21] fastmap_1.2.0      yaml_2.3.8         lifecycle_1.0.4    cyclocomp_1.1.1   
[25] compiler_4.4.0     lintr_3.1.2        pkgconfig_2.0.3    htmlwidgets_1.6.4 
[29] rstudioapi_0.16.0  digest_0.6.35      R6_2.5.1           utf8_1.2.4        
[33] pillar_1.9.0       callr_3.7.6        magrittr_2.0.3     tools_4.4.0       
[37] withr_3.0.0        lazyeval_0.2.2     xml2_1.3.6         remotes_2.5.0     
[41] desc_1.4.3        


  1. Actually, I don’t really care which one you use, but that’s less of a funny take. I prefer the left assignment operator because look! It’s a little arrow! Quirky! Esoteric! An extra keystroke to exercise your fingers!↩︎

  2. We do not talk about <<-.↩︎

  3. Well, not really, because I don’t want you to delete any of your files. But rest assured I’ve included file.remove() in my local version of the function and I’m not afraid to use it.↩︎

  4. In other words, R evaluates this as an object, x, being assigned a numeric value that has an odd number of ‘negative’ symbols that cancel each other out.↩︎