Infix operators I have known and loved

Published
2017-04-09
Tagged

For the last month or so, I’ve been learning the R programming language. It’s been super-interesting, and quite the change from my usual stomping grounds of high-level OO languages like ruby or python.

I’m now past the point of complete beginner, and getting my teeth into some of the more advanced stuff.1 One thing I’ve already had a bunch of fun with, however, is R’s infix operator syntax.

Like a bunch of shiny hinges

I don’t know why I like infix operators. Perhaps it’s R’s lack of per-object methods, resulting in the sort of front-loading of methods that resembles a German dependent clause in reverse:

1
mangled_variable <- mutliate(fold(spindle(variable)))

In a world where functions hang heavy on the left-hand side of your line, infix operators allow you to pace things out - to make sentences out of your code. Hadley Wickam’s magrittr is the most popular (and obvious) example of that:

1
library(magrittr)
2
3
mangled_variable <- variable %>% spindle() %>% fold() %>% mutilate()

…although when overused, it starts to resemble a rambling run-on sentence.

I think that’s really what makes them shine for me: infix operators allow you to change the grammar of your code. And as long as you keep your statements simple, giving the code room to breathe (and your reader room to comprehend), they can turn a stodgy functional morass of parentheses into something a little more…well, human.

The multi-line string

Python has truly spoiled us for multi-line strings:

1
"""
2
Yes, this is a long string that's got more than eighty characters in it.
3
What of it? I can just wrap it across mulitple lines with the python
4
triple-quote operator.
5
"""

Ruby gets into the action as well, although I always feel like I’m somehow poking Matz with a stick whenever I resort to its multi-line string notation:

1
long_string = <<-EOF
2
Here, too, we find ourselves writing long, multi-line strings whose
3
contents exceed the eighty-character limit that tradition and aesthetics
4
have made the default for line length.
5
EOF

But how can we do this in R? I’ve been migrating a lot of data to R from Excel recently, and my predecessors have left informative, useful, historically-relevant comments on data columns. But how do I transcribe a two-hundred-character-long piece of prose?

1
comment(df$Column) <- "This column has a long comment on it, which will be of great use to future programmers and analysts. But adding this comment really breaks that eighty-character limit."

Sure, we can just hard-wrap, if we don’t mind having stray whitespace and newlines all through our code:

1
comment(df$Column) <-
2
  "This column has a long comment on it, which will be of great use to
3
  future programmers and analysts. But adding this comment really breaks
4
  that eighty-character limit."

Or we could edit them all out in post, I guess…

1
remove_whitespace <- function(str){ gsub("[\t\n]","",str) }
2
3
comment(df$Column) <- remove_whitespace(
4
  "This column has a long comment on it, which will be of great use to
5
  future programmers and analysts. But adding this comment really breaks
6
  that eighty-character limit."
7
)

But in my experience, multi-line strings tend to start life as single-line strings, gently growing until they’re pushing up against that 80-character margin, and forcing you to stop and fix them.

R’s base addition operator can’t deal with character strings,2 but we can always make our own…

1
`%+%` <- function(str1, str2){ paste(str1, str2) }
2
3
comment(df$Column) <- 
4
  "This column has a long comment on it, which will be of great use" %+%
5
  "to future programmers and analysts. But adding this comment" %+%
6
  "really breaks that eighty-character limit."

As a side note: since we’re using paste, R will add a space between each of our lines. Which is nice. This notations still isn’t as elegant as python or ruby, but I don’t think I’ll hit anything better until I work out how to write unary operator functions…

Case-insensitve equality

More strings. Excel doesn’t think case is hugely important: case in point, the LOOKUP family of functions will quite happily match foobar against FoOBaR or derivatives. Once I convert a (relatively wide) Excel table into an R data frame, I find myself doing a lot of calls like this:3

1
column <- excel_table$F
2
3
some_variable <-
4
  reference_table %>%
5
  subset(Category == column[5] & UseCase == column[4]) %>%
6
  join(date_range, by="Date", type="right") %>%
7
  use_series("Value")

As soon as your reference table uses a different case for its Category or Use Case qualifiers, though, you’ve got to re-tool that whole subset call:

1
some_variable <-
2
  reference_table %>%
3
  subset(
4
    tolower(Category) == tolower(column[5]) &
5
    tolower(UseCase) == tolower(column[4])
6
  ) %>%
7
  # ...

There’s two points that really jar me here: first, of course, is the fact that this statement has almost doubled in length, but second, we’ve gone from a nice simple infix operator to a couple of bulky functions. Let’s fix that by hiding our implementation away behind a new operator:

1
`%=i%` <- function(a, b){ tolower(a) == tolower(b) }
2
3
some_variable <-
4
  reference_table %>%
5
  subset(Category %=i% column[5] & UseCase %=i% column[4]) %>%
6
  # ...

As a bonus, it’s now a lot easier to switch between case-sensitive and case-insensitive comparisons.

The letter range

As mentioned, I’ve been migrating a lot of data from Excel to R recently. There’s plenty of good libraries for reading data out of Excel files, but when it comes to replicating functions, you generally have to write your own code.

When you’re spending days parsing Excel files, it helps to have your newly-minted data frame’s columns named “A” through whatever column your Excel table goes to. It wasn’t long until I threshed out a quick recursive function for turning integers into Excel-style column letters:

1
int_to_column <- function(int) {
2
  int[int <= 0] <- NA
3
4
  col <- ifelse(
5
    int <= 26,
6
    LETTERS[int],
7
    paste0(int_to_column((int-1) %/% 26), LETTERS[(int-1) %% 26 + 1])
8
  )
9
10
  col[is.na(col)] <- ""
11
}

Which, because this is R and everything is a vector, allows for some pretty cool tricks:

1
> int_to_column(20:30)
2
 [1] "T"  "U"  "V"  "W"  "X"  "Y"  "Z"  "AA" "AB" "AC" "AD"

But what if you want to select columns “CD” through “DA”, say? Sure, you can trial-and-error to work out which numbers these correspond to…or you could make your own letter-style range operator.

First, we need to reverse-engineer our engine that turns numbers into columns:

1
column_to_int <- function(col) {
2
  col <- toupper(col)
3
  col_chars <- nchar(col)
4
5
  ifelse(
6
    col_chars < 2,
7
    as.numeric(Map(function(l){which(l == LETTERS)}, col)),
8
    column_to_int(substring(col,1,col_chars-1))*26 +
9
      column_to_int(substring(col,col_chars))
10
  )
11
}

This allows us to, for example:

1
> column_to_int(c("A", "E", "P", "DD", "EZ"))
2
[1]   1   5  16 108 156

Now we can define our alphabetical range operator:

1
`%:%` <- function(a,b) {
2
  int_to_column(column_to_int(a):column_to_int(b))
3
}

And that, in turn, allows us to perform magic:

1
> "A" %:% "CC"
2
 [1] "A"  "B"  "C"  "D"  "E"  "F"  "G"  "H"  "I"  "J"  "K"  "L"  "M"  "N"  "O"  "P"  "Q"  "R" 
3
[19] "S"  "T"  "U"  "V"  "W"  "X"  "Y"  "Z"  "AA" "AB" "AC" "AD" "AE" "AF" "AG" "AH" "AI" "AJ"
4
[37] "AK" "AL" "AM" "AN" "AO" "AP" "AQ" "AR" "AS" "AT" "AU" "AV" "AW" "AX" "AY" "AZ" "BA" "BB"
5
[55] "BC" "BD" "BE" "BF" "BG" "BH" "BI" "BJ" "BK" "BL" "BM" "BN" "BO" "BP" "BQ" "BR" "BS" "BT"
6
[73] "BU" "BV" "BW" "BX" "BY" "BZ" "CA" "CB" "CC"

  1. I’m looking at you, non-standard evaluation. 

  2. Or at least I don’t think you can - redefining base operators involves a fun trip through the guts of R’s S3 and S4 class systems, a current side-project of mine. 

  3. And, incidentally, it’s long-winded statements like this that I believe really benefit from piping. Try writing this without %>%s and maintain readability, go on.