Este post quizás llega un poco tarde pero hace poco salió la versión 4.1.0 de R. Y si la versión 4.0.0 hizo noticia con el revolucionario cambio de stringAsFactors = FALSE, la gran novedad de esta siguiente versión es la implementación de una “pipa” nativa.

La nueva pipa

La “pipa” (traducción no muy buena de “pipe” en inglés) es una de las principales distinciones del código que usa tidyverse / dplyr. Seguro que alguna vez usaste o viste algo como

library(dplyr) 

mtcars %>%
   group_by(cyl) %>% 
   summarise(mpg = mean(mpg)) 
## # A tibble: 3 x 2
##     cyl   mpg
##   <dbl> <dbl>
## 1     4  26.7
## 2     6  19.7
## 3     8  15.1

Ese %>% es el operador que permite encadenar una función tras otra sin necesidad de asignar variables a pasos intermedios. Técnicamente lo que hace es evaluar la expresión de la derecha (o, más usualmente, la de abajo) usando como primer argumento la expresión de la izquierda (arriba). El paquete dplyr depende del paquete magrittr para hacer esa magia y muchos otros paquetes también importan la pipa de magrittr.

Con la versión 4.1.0, ahora es posible escribir

mtcars |>
   group_by(cyl) |>
   summarise(mpg = mean(mpg))
## # A tibble: 3 x 2
##     cyl   mpg
##   <dbl> <dbl>
## 1     4  26.7
## 2     6  19.7
## 3     8  15.1

¿Cuál es la diferencia, aparte de un caracter menos? (No que la cantidad de caracteres importe mucho si uno usa el atajo de RStudio Ctrl + Shift + M. Y con la nueva versión de RStudio que ahora está en preview, se puede elegir cuál usar.)

La principal, para mí, es que se puede usar la pipa sin depender del paquete magrittr. Quizás esto no sea algo que te quite el sueño, pero como regla general siempre está bueno que tus análisis dependan de la menor cantidad de paquetes distintos, de manera que haya menos posibilidades de que alguno cambie algo importante y se rompa todo.

Para quienes usan dplyr (o quienes van por lo fácil y empiezan con library(tidyverse)) usar |> o %>% probablemente sea indistinto. Pero hay todo un multiverso por fuera del tidyverso. Yo, por ejemplo, prefiero data.table a dplyr y mi sintaxis preferida combina data.table con magrittr. Con este cambio, ya no necesito empezar cada script con library(magrittr) (aunque ver [la siguiente sección]).

Para quienes tienen una (¿insana?) obsesión con la velocidad y la eficiencia, |> parecer ser más rápida que %>%. Esto es porque magrittr hace un montón de cosas detrás de escena, mientras que la pipa nativa es solo una transformación de sintaxis. En otras palabras,

x %>%
   mean()

no es literalmente equivalente a mean(x); hay un montón de procesamiento detrás de esos tres caracteres. Mientas que

x |> 
   mean()

Es interpretado por R exactamente como mean(x). Es decir, hay cero “overhead” por usar |>.

Pero la realidad es que salvo casos especiales, la diferencia es despreciable. En cualquier análisis de datos que valga la pena, el overhead de usar magrittr es minúsculo en comparación con el tiempo que toma hacer (¡y escribir!) el resto de los cómputos. Mi consejo es no prestarle demasiada atención a diferencias del orden del microsegundo.

Lo que sí hay que prestarle atención es a las diferencias sutiles (o no tanto) entre ambas pipas. La más jodida de todas es que la pipa de magrittr tiene una forma de usar un “placeholder” para cuando uno no quiere que el resultado de la parte izquierda vaya al primer argumento de la expresión de la derecha. El ejemplo canónico es el modelo lineal:

mtcars %>% 
   lm(mpg ~ disp, data = .)
## 
## Call:
## lm(formula = mpg ~ disp, data = .)
## 
## Coefficients:
## (Intercept)         disp  
##    29.59985     -0.04122

Como el primer argumento de lm() no son los datos, hay que usar data = . para decirle a magrittr que mtcars no tiene que ser el primer argumento de lm(). La pipa nativa por ahora no tiene placeholder. La forma para solucionar eso es creando una función anónima:

mtcars |> 
   (function(x) lm(mpg ~ cyl, data = x))()
## 
## Call:
## lm(formula = mpg ~ cyl, data = x)
## 
## Coefficients:
## (Intercept)          cyl  
##      37.885       -2.876

Esto es bastante feito, por lo que la propuesta de R es usar otro truco de R 4.1.0: la nueva sintaxis para crear funciones. A partir de R 4.1.0 estas dos expresiones son equivalentes:

function(x) x + 1 
\(x) x + 1

La nueva sintaxis del monigote saludando ( \(x)) esencialmente ahorra caracteres a la hora de crear funciones. Por lo que combinando esto con la pipa nativa, se puede hacer

mtcars |> 
   (\(x) lm(mpg ~ disp, data = x))()
## 
## Call:
## lm(formula = mpg ~ disp, data = x)
## 
## Coefficients:
## (Intercept)         disp  
##    29.59985     -0.04122

que es marginalmente más legible, aunque aún todavía bastante feo. La sintaxis alternativa, que creo que por ahora está en veremos, es esta:

Sys.setenv(`_R_USE_PIPEBIND_` = TRUE) 

mtcars |> 
   . => lm(mpg ~ disp, data = .)
## 
## Call:
## lm(formula = mpg ~ disp, data = mtcars)
## 
## Coefficients:
## (Intercept)         disp  
##    29.59985     -0.04122

(Donde la primera línea es indispensable.)

Como se ve, primero se establece dice cuál es símbolo “placeholder” (en este caso .) y luego del =>, se puede escribir el mismo código que usaría en la pipa de magrittr. En resumen, para los casos donde se usa el . de placeholder, el reemplazo de %>% sería |> . =>. (Aunque, de nuevo, entiendo que esta sintaxis no es definitiva ni oficial.)

¿Y data.table?

Lo cual me lleva a mi amada sintaxis de data.table + magrittr:

mtcars <- data.table::as.data.table(mtcars)
mtcars %>% 
   .[, .(mpg = mean(mpg)), by = cyl]
##    cyl      mpg
## 1:   6 19.74286
## 2:   4 26.66364
## 3:   8 15.10000

Dado que el punto que está al principio de la primera línea no es nada menos que el placeholder de magrittr y que la nueva pipa no tiene placeholder, está más que claro que esta sintaxis no va a funcionar simplemente cambiando %>% por |>. También hay algunas limitaciones, como que la nueva pipa no acepta “símbolos especiales” como [, + o * en la expresión derecha.

En vista de lo anterior, uno pensaría que el cambio sería agregar |> . => , pero no:

mtcars |> 
   . => .[, .(mpg = mean(mpg)), by = cyl]
## Error: function '[' not supported in RHS call of a pipe

Ah, aparece la limitación de esos símbolos especiales. ¿Y ahora?

El truco es que R sólo se fija en el nombre de la función, así que lo único que hay que hacer es renombrar la función [ (amo que en R todo sea una función). Por ejemplo, este código es perfectamente funcional:

.d <- `[` 

mtcars |> 
   .d(, .(mpg = mean(mpg)), by = cyl) 
##    cyl      mpg
## 1:   6 19.74286
## 2:   4 26.66364
## 3:   8 15.10000

Lo cual no es tan malo.

Larga vida a magrittr

Entonces, ¿tengo que desechar a %>% y amar a |>? Y… no necesariamente.

R 4.1.0 salió hace un par de semanas y es muy probable que la mayor parte de la gente no haya actualizado ni tenga planes de actualizar pronto. En ambientes corporativos o en servidores, muchas personas que usan R probablemente ni siquiera tengan control sobre la versión que tienen y quienes lo administran sean bastante reticentes de actualizar. Código “en producción” corriendo en versiones específicas de R en pos de estabilidad y reproducibilidad probablemente tarden años en actualizarse, si es que se actualizan.

Por todo esto, si bien el reinado de magrittr ya tiene los días contados, todavía está lejos de terminar.