Adding strings in R

This started out as a “hey, I wonder…” sort of thing, but as usual, they tend to end up as interesting voyages into the deepest depths of code, so I thought I’d write it up and share. Shoutout to [@coolbutuseless](https://twitter.com/coolbutuseless) for proving that a little curiosity can go a long way and inspiring me to keep digging into interesting topics.

This is what you get if you “glue” “strings”. Photo: https://craftwhack.com/cool-craft-string-easter-eggs/ This is what you get if you “glue” “strings”. Photo: https://craftwhack.com/cool-craft-string-easter-eggs/

This post came across my feed last week, referring to the roperators package on CRAN. In that post, the author introduces an infix operator from that package which ‘adds’ (concatenates/pastes) strings

"using infix (%) operators " %+% "R can do simple string addition"
## [1] "using infix (%) operators R can do simple string addition"

This might be familiar if you use python

"python " + "adds " + "strings"
## 'python adds strings'

or javascript

"javascript " + "also adds " + "strings"
## javascript also adds strings

or perhaps even go

package main

import "fmt"

func main() {
  fmt.Println("go " + "even adds " + "strings")
}
## go even adds strings

or Julia

"julia can " * "add strings"
## "julia can add strings"

but this is not something natively available in R

"this doesn't" + "work"
## Error in "this doesn't" + "work": non-numeric argument to binary operator

Could we make it work, though? That got me wondering. My first guess was to just create a new + function which does allow for this. The normal addition operator is

`+`
## function (e1, e2)  .Primitive("+")

so a first attempt might be

`+` <- function(e1, e2) {
  if (is.character(e1) | is.character(e2)) {
    paste0(e1, e2)
  } else {
    base::`+`(e1, e2)
  }
}

This checks to see if the left or right side of the operator is a character-classed object, and if either is, it pastes the two together. Otherwise it just uses the ‘regular’ addition operator between the two arguments. This works for simple cases, e.g.

"a" + "b"
## [1] "ab"
"a" + 2
## [1] "a2"
2 + 2
## [1] 4
2 + "a"
## [1] "2a"

But we hit an important snag if we try to add to character-represented numbers

"200" + "200"
## [1] "200200"

That’s probably going to be an issue if we read in unformatted data (e.g. from a CSV) as characters and try to treat it like numbers. Normally this would throw the above error about not being numeric, but now we get a silent weird number-character. That’s no good.

An extension to this checks whether or not we have the number-as-a-character situation and falls back to the correct interpretation in that case

`+` <- function(e1, e2) {
  ## unary
  if (missing(e2)) return(e1)
  if (!is.na(suppressWarnings(as.numeric(e1))) & !is.na(suppressWarnings(as.numeric(e2)))) {
    ## both arguments numeric-like but characters
    return(base::`+`(as.numeric(e1), as.numeric(e2)))
  } else if ((is.character(e1) & is.na(suppressWarnings(as.numeric(e1)))) | 
             (is.character(e2) & is.na(suppressWarnings(as.numeric(e2))))) {
    ## at least one true character 
    return(paste0(e1, e2))
  } else {
    ## both numeric
    return(base::`+`(e1, e2))
  }
}

"a" + "b"
## [1] "ab"
"a" + 2
## [1] "a2"
2 + 2
## [1] 4
2 + "a"
## [1] "2a"
"2" + "2"
## [1] 4
2 + "edgy" + 4 + "me"
## [1] "2edgy4me"

So, that’s one option for string addition in R. Is it the right one? The idea of actually dispatching on a character class is inviting. Can we just add a +.character method (since there doesn’t seem to already be one)? Normally when we have S3 dispatch we need a generic function, which calls UseMethod("class"), but we don’t have that in this case. + is an internal generic, which is probably the first sign that we’re going to have trouble. If we try to define the method

`+` <- base::`+`
`+.character` <- function(e1, e2) {
  paste0(e1, e2)
}
"a" + "b"
## Error in "a" + "b": non-numeric argument to binary operator

It seems to fail. What went wrong? Is dispatch not working?

via GIPHY

We want to dispatch on “character” – is that what we have?

class("a")
## [1] "character"

What if we explicitly create an object with that class?

structure("a", class = "character") + 2
## [1] "a2"
2 + structure("a", class = "character")
## [1] "2a"

What if we try to dispatch on some new class?

`+.foo` <- function(e1, e2) {
  paste0(e1, e2)
}

structure("a", class = "foo") + 2
## [1] "a2"

but no dice for just a regular atomic character object. Time to revisit the help pages.

In R, addition is limited to particular classes of objects, defined by the Ops group (there are also Math, Summary, and Complex groups). The methods for the Ops group members describe which classes can be involved in operations involving any of the Ops group members:

"+", "-", "*", "/", "^", "%%", "%/%"
"&", "|", "!"
"==", "!=", "<", "<=", ">=", ">"

These methods are:

eval(.S3methods("Ops"), envir = baseenv())
##  [1] Ops.data.frame      Ops.Date            Ops.difftime       
##  [4] Ops.factor          Ops.numeric_version Ops.ordered        
##  [7] Ops.POSIXt          Ops.quosure*        Ops.raster*        
## [10] Ops.roman*          Ops.ts*             Ops.unit*          
## see '?methods' for accessing help and source code

What’s missing from this list, in order for us to be able to just use “string” + “string” is a character method. What’s perhaps even more surprising is that there is a roman method! Whaaaat?

as.roman("1") + as.roman("5")
## [1] VI
as.roman("2000") + as.roman("18")
## [1] MMXVIII

Since the operations need to be defined for all the members of the Ops group, we would also need to define what to do with, say, * between strings. When one side is a string and the other is a number, a reasonable approach might be that which was taken in the original post (using a new infix %s*%)

"a" %s*% 3
##     a 
## "aaa"

There is, of course, a function to do this already

strrep("a", 3)
## [1] "aaa"

but I could see creating "a" * 3 as a shortcut to this. Note: this exists in python

"a" * 3
## 'aaa'

I don’t know what one would expect "a" * "b" to produce.

The problem with where this is heading is that we aren’t allowed to create the method for an atomic class, as Joris Meys and Brodie Gaslam point out on Twitter

setMethod("+", c("character", "character"), function(e1, e2) paste0(e1, e2))
## Error in setMethod("+", c("character", "character"), function(e1, e2) paste0(e1, : the method for function '+' and signature e1="character", e2="character" is sealed and cannot be re-defined

so no luck there. Brodie also links to a Stack Overflow discussion on this very topic where it is pointed out by Martin Mächler that this has been discussed on r-develq – that makes for some interesting historical weigh-ins on why this isn’t a thing in R. Incidentally, the small-world effect comes into play regarding that Stack Overflow post as one of the three answers happens to be a former work colleague of mine.

So, in the end, it seems the best we can do is the rather long-winded overwrite of + which checks if the arguments really are characters. I don’t mind this, and would probably use it if it was in base R or a package. The biggest issue that people seem to have with this is that it ‘looks like’ addition, but it’s not commutative. If that word is new to you, it just means that x + y should give the same answer as y + x. For numbers, the regular + satisfies this:

2 + 3
## [1] 5
3 + 2
## [1] 5

but when we try to do this with strings… not so much

"a" + "b"
## [1] "ab"
"b" + "a"
## [1] "ba"

This doesn’t particularly bother me, because I’m okay with this not actually being ‘mathematical addition’. The fun turn this then took was the suggestion from Joris Meys that Julia’s non-associative operators is a strength of the language. There, the way that you group values matters

a + b + c is parsed as +(a, b, c) not +(+(a, b), c).

I’ll eventually get around to learning more Julia, but this is already hurting my brain.

That distinction may be of interest, however, to Miles McBain, whose concern was more about repeated applications of + being a bottleneck

In that case, parsing as +("a", "b", "c") is exactly what would be desired.

So, what’s the conclusion of all of this? I’ve learned (and re-learned) a heap more about how the Ops group works, I’ve played a lot with dispatch, and I’ve thought deeply about edge-cases for adding strings. I’ve also been exposed to a bit more Julia. All in all, a worthwhile dive into something potentially silly, but a lot of fun. If you have some thoughts on the matter, leave a comment here or reply on Twitter – I’d love to hear about another angle to this story.


devtools::session_info()

## ─ Session info ──────────────────────────────────────────────────────────
##  setting  value                       
##  version  R version 3.5.2 (2018-12-20)
##  os       Pop!_OS 19.04               
##  system   x86_64, linux-gnu           
##  ui       X11                         
##  language en_AU:en                    
##  collate  en_AU.UTF-8                 
##  ctype    en_AU.UTF-8                 
##  tz       Australia/Adelaide          
##  date     2019-05-19                  
## 
## ─ Packages ──────────────────────────────────────────────────────────────
##  package     * version date       lib source                      
##  assertthat    0.2.0   2017-04-11 [1] CRAN (R 3.5.1)              
##  backports     1.1.3   2018-12-14 [1] CRAN (R 3.5.1)              
##  blogdown      0.10    2019-01-09 [1] CRAN (R 3.5.1)              
##  bookdown      0.9     2018-12-21 [1] CRAN (R 3.5.1)              
##  callr         3.1.1   2018-12-21 [1] CRAN (R 3.5.1)              
##  cli           1.0.1   2018-09-25 [1] CRAN (R 3.5.1)              
##  crayon        1.3.4   2017-09-16 [1] CRAN (R 3.5.1)              
##  desc          1.2.0   2018-05-01 [1] CRAN (R 3.5.1)              
##  devtools      2.0.1   2018-10-26 [1] CRAN (R 3.5.1)              
##  digest        0.6.18  2018-10-10 [1] CRAN (R 3.5.1)              
##  dplyr       * 0.8.0.1 2019-02-15 [1] CRAN (R 3.5.1)              
##  evaluate      0.13    2019-02-12 [1] CRAN (R 3.5.1)              
##  forcats     * 0.4.0   2019-02-17 [1] CRAN (R 3.5.1)              
##  fs            1.2.6   2018-08-23 [1] CRAN (R 3.5.1)              
##  glue          1.3.0   2018-07-17 [1] CRAN (R 3.5.1)              
##  htmltools     0.3.6   2017-04-28 [1] CRAN (R 3.5.1)              
##  jsonlite      1.6     2018-12-07 [1] CRAN (R 3.5.1)              
##  knitr         1.22.5  2019-03-23 [1] Github (yihui/knitr@072253d)
##  lattice       0.20-38 2018-11-04 [1] CRAN (R 3.5.1)              
##  magrittr      1.5     2014-11-22 [1] CRAN (R 3.5.1)              
##  Matrix        1.2-16  2019-03-08 [1] CRAN (R 3.5.1)              
##  memoise       1.1.0   2017-04-21 [1] CRAN (R 3.5.1)              
##  pillar        1.3.1   2018-12-15 [1] CRAN (R 3.5.1)              
##  pkgbuild      1.0.2   2018-10-16 [1] CRAN (R 3.5.1)              
##  pkgconfig     2.0.2   2018-08-16 [1] CRAN (R 3.5.1)              
##  pkgload       1.0.2   2018-10-29 [1] CRAN (R 3.5.1)              
##  prettyunits   1.0.2   2015-07-13 [1] CRAN (R 3.5.1)              
##  processx      3.2.1   2018-12-05 [1] CRAN (R 3.5.1)              
##  ps            1.3.0   2018-12-21 [1] CRAN (R 3.5.1)              
##  purrr         0.3.0   2019-01-27 [1] CRAN (R 3.5.1)              
##  R6            2.4.0   2019-02-14 [1] CRAN (R 3.5.1)              
##  Rcpp          1.0.0   2018-11-07 [1] CRAN (R 3.5.1)              
##  remotes       2.0.2   2018-10-30 [1] CRAN (R 3.5.1)              
##  reticulate    1.11.1  2019-03-06 [1] CRAN (R 3.5.1)              
##  rlang         0.3.1   2019-01-08 [1] CRAN (R 3.5.1)              
##  rmarkdown     1.11    2018-12-08 [1] CRAN (R 3.5.1)              
##  roperators  * 1.1.0   2018-09-28 [1] CRAN (R 3.5.1)              
##  rprojroot     1.3-2   2018-01-03 [1] CRAN (R 3.5.1)              
##  sessioninfo   1.1.1   2018-11-05 [1] CRAN (R 3.5.1)              
##  stringi       1.3.1   2019-02-13 [1] CRAN (R 3.5.1)              
##  stringr       1.4.0   2019-02-10 [1] CRAN (R 3.5.1)              
##  tibble        2.0.1   2019-01-12 [1] CRAN (R 3.5.1)              
##  tidyselect    0.2.5   2018-10-11 [1] CRAN (R 3.5.1)              
##  usethis       1.4.0   2018-08-14 [1] CRAN (R 3.5.1)              
##  withr         2.1.2   2018-03-15 [1] CRAN (R 3.5.1)              
##  xfun          0.5     2019-02-20 [1] CRAN (R 3.5.1)              
##  yaml          2.2.0   2018-07-25 [1] CRAN (R 3.5.1)              
## 
## [1] /home/jono/R/x86_64-pc-linux-gnu-library/3.5
## [2] /usr/local/lib/R/site-library
## [3] /usr/lib/R/site-library
## [4] /usr/lib/R/library


rstats 

See also