Provavelmente você já ouviu falar do operador pipe (%>%
). Muita gente acha
que ele é uma sequência mágica de símbolos que muda completamente o visual do
seu código, mas na verdade ele não passa de uma função como outra qualquer.
Nesse post vou explorar um pouco da história do pipe, como ele funciona e por que utilizá-lo.
Origem
O conceito de pipe existe pelo menos desde os anos 1970. De acordo com seu criador, 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
Por essa descrição já conseguimos 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 portugês o termo é traduzido como “canalização” ou “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 nosso tão querido R. Ele pode ter múltiplos disfarces, mas o seu objetivo é sempre o mesmo: transportar resultados.
Como funciona
Em R o pipe tem uma cara meio estranha (%>%
), 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: porque no R uma função infixa só pode ser
declarada assim.
Vamos começar demonstrando sua funcionalidade básica. Carregue o pacote
magrittr
e declare o pipe usando Ctrl + Shift + M.
library(magrittr)
`%>%`("oi", print)
#> [1] "oi"
Não ligue para os acentos graves em volta do pipe, o comando acima só serve
para demonstrar que ele não é nada mais que uma função; perceba que o seu
primeiro argumento ("oi"
) virou a entrada do seu segundo argumento (print
).
"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 está acontecendo primeiro? A linha relevante começa com a divisão por 2, depois vem a soma com 3 e, por fim, os valores de entrada.
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\).
Isso pode ser realizado se tivermos uma função que passa o resultado do que está à sua esquerda para a função que está à sua direita…
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.
Você pode já ter notado isso, mas a entrada (esquerda) de um pipe sempre é passada como o primeiro argumento agumento da sua saída (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
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 debugabilidade! Se tivermos encontrado um bug na pipeline, basta executar linha a linha do encadeamento até que encontremos a linha problemática. Com o pipe podemos 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 não é uma obrigatoriedade. Com um 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
Bônus
Agora que você já sabe dos usos mais comuns do pipe, aqui está uma outra
funcionalidade interessante: 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.
Conclusão
O pipe não é apenas algo que deve ser usado pelos fãs do tidyverse. Ele é uma função extremamente útil que ajuda na legibilidade e programação de código, independentemente de quais pacotes utilizamos.
Se quiser saber mais sobre o mundo do pipe, leia este post do Daniel sobre o Manifesto Tidy e o nosso tutorial mais aprofundado sobre o próprio pipe.