Loodgieters, ketens en beroemde schilders: De (bijgewerkte) geschiedenis van de pipe-operator in R

Opmerking: Dit bericht is een bijgewerkte versie van een artikel dat in 2014 is gepubliceerd in een eerdere versie van deze site.

Ons verhaal begint, zoals veel andere R-gerelateerde verhalen, op StackOverflow. Op 17 januari 2012 stelde een anonieme gebruiker, "user4", de volgende vraag:

Hoe kun je de forward pipe-operator van F# implementeren in R? De operator maakt het mogelijk om eenvoudig een reeks berekeningen aan elkaar te koppelen. Bijvoorbeeld, wanneer je inputdata hebt en de functies foo en bar opeenvolgend wilt aanroepen, kun je schrijven: data |> foo |> bar

Ben Bolker antwoordde dezelfde dag en leverde wat beschouwd kan worden als de eerste pipe in R:

"%>%" <- function(x,f) do.call(f,list(x))
pi %>% sin %>% cos
## [1] 1

Hoewel dit een vroege poging was, vond de eerste formele introductie van een pipe-functie in een R-pakket waarschijnlijk plaats op 5 april 2012. Peter Meilstrup startte toen het pakket ptools als een verzameling van diverse utilities voor datamanipulatie en programmeren. Een van deze utilities was de functie chain, die een manier implementeerde om argumenten te "pipen". Het pakket werd later omgedoopt tot vadr, waar de chain-functie nog steeds te vinden is.

De opkomst van dplyr

Kort daarna werd een van de pakketten gecreëerd die de populariteit van het gebruik van de pipe aanzienlijk zou vergroten. Op 28 oktober 2012 startte Hadley Wickham het dplyr-project op GitHub als een evolutie van zijn data-analysepakket plyr (aanvankelijk heette het pakket inderdaad plyr2).

Oorspronkelijk bevatte dplyr geen chain-functie, maar dit veranderde op 9 oktober 2013 met de introductie van de eerste pipe in dplyr. De functie heette ook chain, maar het pakket introduceerde nu een eigen operator voor de pipe: %.%. Het idee achter de introductie van chain was om de notatie te vereenvoudigen bij het toepassen van meerdere functies op een data frame. Zonder de chain-functie moest men in dplyr de functies van binnen naar buiten lezen:

library(hflights)
library(dplyr)

filter(
  summarise(
    select(
      group_by(hflights, Year, Month, DayofMonth),
      Year:DayofMonth, ArrDelay, DepDelay
    ),
    arr = mean(ArrDelay, na.rm = TRUE),
    dep = mean(DepDelay, na.rm = TRUE)
  ),
  arr > 30 | dep > 30
)
## # A tibble: 14 × 5
## # Groups:   Year, Month [10]
##     Year Month DayofMonth   arr   dep
##    <int> <int>      <int> <dbl> <dbl>
##  1  2011     2          4  44.1  47.2
##  2  2011     3          3  35.1  38.2
##  3  2011     3         14  46.6  36.1
##  4  2011     4          4  38.7  27.9
##  5  2011     4         25  37.8  22.3
##  6  2011     5         12  69.5  64.5
##  7  2011     5         20  37.0  26.6
##  8  2011     6         22  65.5  62.3
##  9  2011     7         29  29.6  31.9
## 10  2011     9         29  39.2  32.5
## 11  2011    10          9  61.9  59.5
## 12  2011    11         15  43.7  39.2
## 13  2011    12         29  26.3  30.8
## 14  2011    12         31  46.5  54.2

Met de introductie van de chain-functie veranderde dit naar:

chain(
  hflights,
  group_by(Year, Month, DayofMonth),
  select(Year:DayofMonth, ArrDelay, DepDelay),
  summarise(
    arr = mean(ArrDelay, na.rm = TRUE),
    dep = mean(DepDelay, na.rm = TRUE)
  ),
  filter(arr > 30 | dep > 30)
)

En met de %.% pipe werd het als volgt:

hflights %.%
  group_by(Year, Month, DayofMonth) %.%
  select(Year:DayofMonth, ArrDelay, DepDelay) %.%
  summarise(
    arr = mean(ArrDelay, na.rm = TRUE),
    dep = mean(DepDelay, na.rm = TRUE)
  ) %.%
  filter(arr > 30 | dep > 30)

De komst van magrittr

De %.% pipe zou echter niet lang in het dplyr-pakket blijven. Op 29 december 2013 bezocht Stefan Bache de oude StackOverflow-vraag opnieuw en stelde hij een alternatief voor het oorspronkelijke antwoord voor:

`%>%` <- function(e1, e2)
{
  cl <- match.call()
  e  <- do.call(substitute, list(cl[[3]], list(. = cl[[2]])))
  eval(e)
}

Dit maakte een chain zoals deze mogelijk:

iris %>%
  subset(., Species == "setosa", select = -Species) %>%
  colMeans(.)
## Sepal.Length  Sepal.Width Petal.Length  Petal.Width
##        5.006        3.428        1.462        0.246

Stefan ging verder met de ontwikkeling van deze pipe-operatie en implementeerde op 30 december 2013 het pakket plumbr op GitHub, waarin de %>% operator was opgenomen. Twee dagen later werd plumbr omgedoopt tot magrittr, een duidelijke verwijzing naar het beroemde schilderij "La Trahison des images" van de Belgische schilder René Magritte.

Het dplyr-pakket werd parallel ontwikkeld, maar deze twee ontwikkelingen waren bedoeld om samen te smelten. In de eerste reactie op de aankondiging van dplyr via de RStudio-blog in januari 2014, liet Stefan Bache Hadley Wickham weten dat de magrittr-operator bestond.

Vervolgens gebeurde het volgende:

  • 19 maart 2014: De chain-functie werd deprecated in dplyr.
  • 14 april 2014: dplyr incorporeerde de %>% operator van magrittr en adviseerde deze als vervanging voor %.%, omdat de nieuwe operator gemakkelijker te typen is door de Shift-toets ingedrukt te houden.
  • 1 augustus 2014: De %.% operator werd officieel deprecated.

Tegelijkertijd, op 31 juli 2014, voegde RStudio IDE versie v0.98.987 een sneltoets toe voor de dplyr/magrittr pipe-operator %>% (aanvankelijk Shift+Alt+.), wat later werd gewijzigd naar de huidige vorm: Ctrl+Shift+M.

Andere iteraties: pipeR en wrapr

Er waren ook andere pogingen om de pipe in R te implementeren. Op 7 april 2014 publiceerde Kun Ren het pakket pipeR op GitHub, met de operator %>>% voor meer flexibiliteit. Dit pakket bood verschillende functies:

  • Gebruik als eerste argument van een functie.
  • Gebruik als argument in een expressie (met behulp van .).
  • Gebruik via een formule.
  • Opslaan van tussenresultaten (met behulp van ~).
  • Extraheren van elementen op naam (met behulp van ()).

Voorbeeld van pipeR:

library(pipeR)
mtcars %>>%
  (lm(mpg ~ wt + cyl, data = .)) %>>%
  (~ lm_mtcars) %>>%
  summary %>>%
  (r.squared)
## [1] 0.8302274

Hoewel pipeR sinds 2016 niet meer is bijgewerkt, is het nog steeds beschikbaar op CRAN en GitHub.

In juli 2018 introduceerde het pakket wrapr de "dot arrow pipe" %.%, waarbij de notatie a %.>% b bijna gelijkstaat aan . <- a; b.

Voorbeeld van wrapr:

library(wrapr)
1:10 %.>% mean(.)
## [1] 5.5

Hierbij blijft de linkerzijde (lhs) bewaard in de omgeving:

.
##  [1]  1  2  3  4  5  6  7  8  9 10

De integratie in Base R

Pipes hebben de manier waarop we in R coderen fundamenteel veranderd door code eenvoudiger en leesbaarder te maken. Het was dan ook slechts een kwestie van tijd voordat ze in Base R zouden worden opgenomen.

Een van de eerste pogingen kwam van Lionel Henry, die in februari 2016 begon met het testen van een native pipe in Base R. Hij bepleitte dat een native implementatie de foutopvang (error recovery) zou verbeteren, omdat men dan niet meer afhankelijk was van een extern pakket zoals magrittr.

Na een periode van schijnbare stilte volgde op 5 oktober 2019 een discussie op de r-devel mailinglijst, aangewakkerd door een vraag van Antoine F. over of Base R een piping-operator zou moeten hebben. Dit leidde tot reacties van diverse R-ontwikkelaars, waaronder Lionel Henry, Duncan Murdoch en Luke Tierney.

Tijdens de keynote van UseR! 2020 spraken Luke Tierney en Martin Mächler over nieuwe ontwikkelingen in R. Tierney kondigde aan dat er, gebaseerd op ideeën van Lionel Henry en Jim Hester, een base pipe |> in ontwikkeling was.

Aan het eind van 2020 werd |> officieel aangekondigd in de devel-versie van R. Uiteindelijk verscheen de native pipe in de stabiele versie R 4.1.0, uitgebracht op 18 mei 2021.

Het eerdere voorbeeld uit de dplyr-documentatie kan nu zo worden geschreven:

hflights |>
  group_by(Year, Month, DayofMonth) |>
  select(Year:DayofMonth, ArrDelay, DepDelay) |>
  summarise(
    arr = mean(ArrDelay, na.rm = TRUE),
    dep = mean(DepDelay, na.rm = TRUE)
  ) |>
  filter(arr > 30 | dep > 30)

Vergelijking: magrittr vs. Base R

Parallel aan de native pipe bleef magrittr evolueren. In november 2020 verscheen versie 2.0, waarbij de %>% pipe in C werd geschreven voor een significante snelheidswinst.

Er zijn echter belangrijke verschillen tussen de magrittr pipe en de native pipe:

1. Haakjes bij functies

Bij magrittr zijn haakjes aan het einde van functienamen niet altijd nodig, terwijl dit bij de native pipe verplicht is.

library(magrittr)
1:10 %>% mean() # Werkt
1:10 |> mean()  # Werkt
1:10 %>% mean   # Werkt
1:10 |> mean    # Fout: The pipe operator requires a function call as RHS

2. De placeholder (.)

Standaard sturen beide pipes de linkerzijde (lhs) door als eerste argument naar de rechterzijde (rhs). Met magrittr kun je echter de punt (.) gebruiken om de lhs op een andere positie in de functie te plaatsen.

Dit werkt met magrittr:

library(palmerpenguins)
library(rpart)
library(rpart.plot)

penguins %>%
  rpart(species ~ ., method = "class", data = .) %>%
  prp(extra = 4)

Dit werkt niet met de native pipe:

penguins |>
  rpart(species ~ ., method = "class", data = .) |>
  prp(extra = 4)
## Error in model.frame.default... 'data' must be a data.frame, environment, or list

Om dit in Base R te bereiken, moet men gebruikmaken van de shorthand inline functie-notatie \(x) die in R 4.1.0 is geïntroduceerd:

penguins |>
  {\(x) rpart(species ~ ., method = "class", data = x)}() |>
  prp(extra = 4)

Hoewel er suggesties zijn geweest om een nieuwe pipe in Base R te introduceren die de lhs naar andere argumenten kan sturen (bijvoorbeeld met _ als placeholder), is dit nog niet officieel ondersteund.

***

Negen jaar na de eerste vraag op StackOverflow is de wens van "user4" om data |> foo |> bar te kunnen schrijven volledig gerealiseerd. Het heeft voor velen van ons de manier van coderen veranderd. Ik ben benieuwd wat versies 5, 6 of 7 ons zullen brengen.

Aanvullende opmerkingen

  • Het magrittr pakket implementeert ook drie andere pipes: %T>%, %$% en %<>%.
  • Het data.table pakket heeft geen algemene pipe, maar je kunt functies koppelen met ][.
  • Er is een verschil in uitspraak: waar Hadley Wickham stelt dat %>% uitgesproken moet worden als "then", noemt Luke Tierney |> in zijn keynote "goes into".
  • De pipe |> is ook geïmplementeerd in andere talen zoals Elixir, Julia, Javascript en F#.
  • De pipe |> werd oorspronkelijk al in mei 1994 voorgesteld door Tobias Nipkow voor de proof assistant Isabelle/ML.