7  Le pipe

Il est fréquent d’enchaîner des opérations en appelant successivement des fonctions sur le résultat de l’appel précédent.

Prenons un exemple. Supposons que nous ayons un vecteur numérique v dont nous voulons calculer la moyenne puis l’afficher via un message dans la console. Pour un meilleur rendu, nous allons arrondir la moyenne à une décimale, mettre en forme le résultat à la française, c’est-à-dire avec la virgule comme séparateur des décimales, créer une phrase avec le résultat, puis l’afficher dans la console. Voici le code correspondant, étape par étape.

v <- c(1.2, 8.7, 5.6, 11.4)
m <- mean(v)
r <- round(m, digits = 1)
f <- format(r, decimal.mark = ",")
p <- paste0("La moyenne est de ", f, ".")
message(p)
La moyenne est de 6,7.

Cette écriture, n’est pas vraiment optimale, car cela entraîne la création d’un grand nombre de variables intermédiaires totalement inutiles. Nous pourrions dès lors imbriquer les différentes fonctions les unes dans les autres :

message(paste0("La moyenne est de ", format(round(mean(v),        digits = 1), decimal.mark = ","), "."))
La moyenne est de 6,7.

Nous obtenons bien le même résultat, mais la lecture de cette ligne de code est assez difficile et il n’est pas aisé de bien identifier à quelle fonction est rattaché chaque argument.

Une amélioration possible serait d’effectuer des retours à la ligne avec une indentation adéquate pour rendre cela plus lisible.

message(
  paste0(
    "La moyenne est de ", 
    format(
      round(
        mean(v), 
        digits = 1), 
      decimal.mark = ","
    ),
    "."
  )
)
La moyenne est de 6,7.

C’est déjà mieux, mais toujours pas optimal.

7.1 Le pipe natif de R : |>

Depuis la version 4.1, R a introduit ce que l’on nomme un pipe (tuyau en anglais), un nouvel opérateur noté |>.

Le principe de cet opérateur est de passer l’élément situé à sa gauche comme premier argument de la fonction située à sa droite. Ainsi, l’écriture x |> f() est équivalente à f(x) et l’écriture x |> f(y) à f(x, y).

Parfois, on souhaite passer l’objet x à un autre endroit de la fonction f() que le premier argument. Depuis la version 4.2, R a introduit l’opérateur _,que l’on nomme un placeholder, pour indiquer où passer l’objet de gauche. Ainsi, x |> f(y, a = _) devient équivalent à f(y, a = x). ATTENTION : le placeholder doit impérativement être transmis à un argument nommé !

Tout cela semble encore un peu abstrait ? Reprenons notre exemple précédent et réécrivons le code avec le pipe.

v |> 
  mean() |> 
  round(digits = 1) |> 
  format(decimal.mark = ",") |> 
  paste0("La moyenne est de ", m = _, ".") |> 
  message()
La moyenne est de 6,7.

Le code n’est-il pas plus lisible ?

Le diaporama ci-dessous vous permet de visualiser chaque étape du code.

7.2 Le pipe du tidyverse : %>%

Ce n’est qu’à partir de la version 4.1 sortie en 2021 que R a proposé de manière native un pipe, en l’occurence l’opérateur |>.

En cela, R s’est notamment inspiré d’un opérateur similaire introduit dès 2014 dans le tidyverse. Le pipe du tidyverse fonctionne de manière similaire. Il est implémenté dans le package magrittr qui doit donc être chargé en mémoire. Le pipe est également disponible lorsque l’on effectue library(tidyverse).

Cet opérateur s’écrit %>% et il dispose lui aussi d’un placeholder qui est le .. La syntaxe du placeholder est un peu plus souple puisqu’il peut être passé à tout type d’argument, y compris un argument sans nom. Si l’on reprend notre exemple précédent.

library(magrittr)
v %>% 
  mean() %>%
  round(digits = 1) %>%
  format(decimal.mark = ",") %>%
  paste0("La moyenne est de ", ., ".") %>%
  message()
La moyenne est de 6,7.

7.3 Vaut-il mieux utiliser |> ou %>% ?

Bonne question. Si vous utilisez une version récente de R (≥ 4.2), il est préférable d’avoir recours au pipe natif de R dans la mesure où il est plus efficient en termes de temps de calcul car il fait partie intégrante du langage. Dans ce guide, nous privilégions d’ailleurs l’utilisation de |>.

Si votre code nécessite de fonctionner avec différentes versions de R, par exemple dans le cadre d’un package, il est alors préférable, pour le moment, d’utiliser celui fourni par magrittr (%>%).

7.4 Accéder à un élément avec purrr::pluck() et purrr::chuck()

Il est fréquent d’avoir besoin d’accéder à un élément précis d’une liste, d’un tableau ou d’un vecteur, ce que l’on fait d’ordinaire avec la syntaxe [[]] ou $ pour les listes ou [] pour les vecteurs. Cependant, cette syntaxe se combine souvent mal avec un enchaînement d’opérations utilisant le pipe.

Le package purrr, chargé par défaut avec library(tidyverse), fournit une fonction purrr::pluck() qui, est l’équivalent de [[]], et qui permet de récupérer un élément par son nom ou sa position. Ainsi, si l’on considère le tableau de données iris, pluck(iris, "Petal.Witdh") est équivalent à iris$Petal.Width. Voyons un exemple d’utilisation dans le cadre d’un enchaînement d’opérations.

iris |> 
  purrr::pluck("Petal.Width") |> 
  mean()
[1] 1.199333

Cette écriture est équivalente à :

mean(iris$Petal.Width)
[1] 1.199333

purrr::pluck() fonctionne également sur des vecteurs (et dans ce cas opère comme []).

v <- c("a", "b", "c", "d")
v |> purrr::pluck(2)
[1] "b"
v[2]
[1] "b"

On peut également, dans un même appel à purrr::pluck(), enchaîner plusieurs niveaux. Les trois syntaxes ci-après sont ainsi équivalents :

iris |> 
  purrr::pluck("Sepal.Width", 3)
[1] 3.2
iris |> 
  purrr::pluck("Sepal.Width") |> 
  purrr::pluck(3)
[1] 3.2
iris[["Sepal.Width"]][3]
[1] 3.2

Si l’on demande un élément qui n’existe pas, purrr:pluck() renverra l’élement vide (NULL). Si l’on souhaite plutôt que cela génère une erreur, on aura alors recours à purrr::chuck().

iris |> purrr::pluck("inconnu")
NULL
iris |> purrr::chuck("inconnu")
Error in `purrr::chuck()`:
! Can't find name `inconnu` in vector.
v |> purrr::pluck(10)
NULL
v |> purrr::chuck(10)
Error in `purrr::chuck()`:
! Index 1 exceeds the length of plucked object (10 > 4).