7 Pipe

Dificilmente um programador de R passa muito tempo sem ouvir falar do operador pipe (%>%). Saber o que ele faz e significa, no entanto, é algo mais complexo. Apesar de não passar de uma função como outra qualquer, os efeitos que ele pode ter no visual e na compreensibilidade de um código são imensos.

Entender o pipe em profundidade pode levar muito tempo, mas o básico já é suficiente para a maioria das pessoas. É importante ter pelo menos uma ideia do que ele faz caso você acabe se deparando com um código que o utiliza e, quem sabe um dia, usá-lo nos seus próprios programas.

No fundo, o conceito de pipe existe pelo menos desde os anos 1970. De acordo com seu criador, Douglas McIlroy, o operador foi concebido em “uma noite febril” e tinha o objetivo de simplificar comandos cujos resultados deveriam ser passados para outros comandos.

ls | cat
# Desktop
# Documents
# Downloads
# Music
# Pictures
# Public
# Templates
# Videos

Com esse exemplo já é possível ter uma ideia de onde vem o seu nome: pipe em inglês significa “cano”, referindo-se ao transporte das saídas dos comandos. Em português o termo é traduzido preferencialmente como “encadeamento”, mas no dia-a-dia é mais comum usar o termo em inglês.

A partir daí o pipe tem aparecido nas mais diversas aplicações, desde HTML até o próprio R. Ele pode aparecer em diferentes formas, mas o seu objetivo é sempre o mesmo: canalizar resultados de um comando para o outro.

7.1 Como funciona

Em R, o pipe tem uma aparência bastante particular (%>%), mas no fundo ele não passa de uma função infixa, ou seja, uma função que aparece entre os seus argumentos (como a + b ou a %in% b). Na verdade é por isso mesmo que ele tem porcentagens antes e depois: no R uma função infixa só pode ser declarada assim (vide o próprio %in%()).

Se você estiver no RStudio, como sugerido anteriormente, para usar o pipe basta carregar a biblioteca magrittr e utilizar o atalho Ctrl + Shift + M; essas inocentes teclas irão fazê-lo aparecer magicamente diante dos seus olhos. Abaixo é possível notar como o pipe não passa de uma função como outra qualquer (ignore os acentos graves):

library(magrittr)

`%>%`("oi", print)
#> [1] "oi"

Perceba que, no código acima, o primeiro argumento do pipe ("oi") virou a entrada do seu segundo argumento (a função print()). Abaixo o pipe está na sua forma mais tradicional entre seus dois argumentos:

"oi" %>% print()
#> [1] "oi"

Observe agora o comando abaixo. Queremos primeiro somar 3 a uma sequência de números e depois dividí-los por 2:

mais_tres <- function(x) { x + 3 }
sobre_dois <- function(x) { x / 2 }

x <- 1:3

sobre_dois(mais_tres(x))
#> [1] 2.0 2.5 3.0

Perceba como fica difícil de entender o que acontece primeiro. A linha relevante começa com a divisão por 2, depois vem a soma com 3 e, ao fim, os valores de entrada. É exatamente o oposto da nossa ordem de leitura da esquerda para a direita.

Nesse tipo de situação é mais legível usar a notação de composição de funções, com as funções sendo exibidas na ordem em que serão aplicadas: \(f \circ g\). Não é necessário conhecer essa notação, basta imaginar quão mais legível ficaria aquele comando se houvesse algum recurso que passasse o que o resultado do que está à sua esquerda para a função que está à sua direita. Esse é o pipe.

x %>% mais_tres() %>% sobre_dois()
#> [1] 2.0 2.5 3.0

No comando acima fica evidente que pegamos o objeto x, somamos 3 e dividimos por 2.

Perceba que a entrada de um pipe (esquerda) sempre é passada como o primeiro argumento da função à sua direita. Isso não impede que as funções utilizadas em uma sequência de pipes tenham outros argumentos.

mais_n <- function(x, n) { x + n }

x %>% mais_n(4) %>% sobre_dois()
#> [1] 2.5 3.0 3.5

7.2 Vantagens

A grande vantagem do pipe não é só enxergar quais funções são aplicadas primeiro, mas sim nos ajudar a programar pipelines (“encanamento” em inglês) de tratamentos de dados.

library(dplyr)

starwars %>% 
  mutate(bmi = mass/((height/100)^2)) %>%
  select(name, bmi, species) %>%
  group_by(species) %>%
  summarise(bmi = mean(bmi))
#> # A tibble: 38 x 2
#>    species     bmi
#>    <chr>     <dbl>
#>  1 Aleena     24.0
#>  2 Besalisk   26.0
#>  3 Cerean     20.9
#>  4 Chagrian   NA  
#>  5 Clawdite   19.5
#>  6 Droid      NA  
#>  7 Dug        31.9
#>  8 Ewok       25.8
#>  9 Geonosian  23.9
#> 10 Gungan     NA  
#> # ... with 28 more rows

Acima fica extremamente claro o que está acontecendo em cada passo da pipeline. Partindo da base starwars, primeiro transformamos, depois selecionamos, agrupamos e resumimos: em cada linha temos uma operação e elas são executadas em sequência.

Isso não melhora só a legibilidade do código, mas também a sua debugabilidade4. Se tivermos encontrado um bug na pipeline, basta executar o encadeamento linha a linha até que encontremos o comando problemático. Com o pipe é possível programar de forma mais compacta, legível e correta.

Todos os exemplos acima envolvem passar a entrada do pipe como o primeiro argumento da função à direita, mas esta não é uma obrigatoriedade. Com o operador placeholder . podemos indicar exatamente onde deve ser colocado o valor que chega no pipe:

y_menos_x <- function(x, y) { y - x }

x %>%
  mais_tres() %>%
  purrr::map2(4:6, ., y_menos_x)
# [[1]]
# [1] 0
# 
# [[2]]
# [1] 0
# 
# [[3]]
# [1] 0

Outra funcionalidade interessante e pouco conhecida do pipe são as funções unárias. Se você estiver familiarizado com o pacote purrr, esse é um jeito bastante simples de criar funções descartáveis.

m3_s2 <- . %>%
  mais_tres() %>%
  sobre_dois()

m3_s2(x)
#> [1] 2.0 2.5 3.0

Usando novamente o . definimos uma função que recebe apenas um argumento com uma sequência de aplicações de outras funções. Mas não se preocupe se as funções unárias tiverem parecido algo de outro mundo porque é realmente muito raro encontrá-las nos código alheios.

Por fim, caso você queira utilizar o pipe dentro do seu próprio pacote, basta executar uma simples função do usethis: use_pipe(). Ela adiciona o magrittr como uma dependência do seu pacote e faz com que o pipe seja acessível dentro do mesmo, o que realmente facilita o trabalho do desenvolvedor que não quer entender o significado do código a seguir:

#' Pipe operator
#'
#' See \code{magrittr::\link[magrittr:pipe]{\%>\%}} for details.
#'
#' @name %>%
#' @rdname pipe
#' @keywords internal
#' @export
#' @importFrom magrittr %>%
#' @usage lhs \%>\% rhs
NULL

Se você não estiver trabalhando em um pacote, mas também não quer carregar o pacote com library(), há duas formas de trazer o pipe para os seus scripts: puxando ele como uma função ou usando o argumento include.only da library().

# Puxando como função
`%>%` <- magrittr::`%>%`

# Usando include.only
library(magrittr, include.only = "%>%")

  1. Ainda não encontrei um termo melhor em português para esse conceito↩︎