Un code qui ronronne avec purrr

Si vous suivez régulièrement les nouveautés de l’univeRs, un package du tidyverse est certainement remonté à vos oreilles ces derniers mois : purrr. « Encore un, pas facile de tenir la cadence ? » vous dites-vous ? Laissez-vous guider, nous sommes là pour vous présenter ce package ronronnant ! 

« Make your pure functions purr with the ‘purrr’ package », entame la description de purrr, un des packages de l’ordocosme. Bien que le jeu sur les sonorités sonne bien dans la langue de Shakespeare, l’explication n’est pas claire de prime abord. « This package completes R’s functional programming tools with missing features present in other programming languages ». Et donc, en clair ?

Functionnal programming, késako ?

La philosophie de la programmation fonctionnelle pourrait couvrir des pages et des pages de ce blog, et les discussions et articles développant en profondeur ce concept ne manquent pas. Nous tenterons donc de faire court.

Function

Comme son nom l’indique, ce modèle de programmation se concentre sur l’écriture de fonctions, indépendamment des données qui lui sont externes. Autrement dit, si vous ne deviez retenir qu’une règle de ce paradigme, ce serait celle-ci : la programmation fonctionnelle se caractérise par l’utilisation de fonctions « pures », c’est-à-dire des fonctions opérant en l’absence de ‘side effects — qui ne se reposent pas sur des données externes, et qui ne changent pas les données qui existent en dehors.

Ainsi, la programmation fonctionnelle implique que les données soient immuables : les données d’origine ne sont jamais modifiées, et chaque modification crée de nouvelles données. Ce type de programmation est également dit « stateless », c’est-à-dire que les fonctions doivent pouvoir opérer indépendamment de leur environnement, sans reposer sur des données venant de l’extérieure de la fonction.

Bien, et purrr dans tout ça ?

L’une des quêtes d’Hadley Wickham (hormis celle de la démocratisation des tidy data) est l’optimisation de la programmation avec R. Parmi ses outils, dplyr, célèbre package facilitant la manipulation de tableaux. Si vous ne le connaissez pas encore, nous en parlions sur ce blog il y a quelque temps.

Pourtant, malgré la puissance de dplyr, sa limitation au format tabulaire peut complexifier l’écriture de certains scripts : que faire si l’on analyse un appel à une API revenant en format JSON, et donc sous forme de liste ? Pour ne pas intégrer une étape de transformation des données en tableau (et donc d’éventuelles mauvaises manipulations), faites appel à purrr, un package vous permettant d’exécuter des fonctions de manière récursive sur tous les éléments d’un vecteur.

Keep calm and purrr on

Et en pratique ?

Toutes les fonctions de purrr sont construites en suivant un même schéma : function(.x, .f) où :

.x est un vecteur

.f est une fonction pure

le résultat est toujours un vecteur du même type que .x

La famille principale de ce package est celle des map, fonctions appliquant de manière récursive la fonction .f sur tous les éléments de .x, un élément de type liste. Des déclinaisons de map existent pour chaque type de vecteur : map_dbl(), où .x est de type dbl, map_lgl(), pour les vecteurs logiques, map_int() pour les integers, map_df() pour les dataframes, et map_chr(), vous l’aurez deviné, pour les vecteurs de type characters.

Et pour encore plus de flexibilité dans l’écriture, ces appels à map supportent la création de fonctions anonymes pour .f — avec la possibilité d’utiliser ~ en lieu et place de function(x). Autrement dit, un : map(df, function(x) sum(is.na(x))) pourra s’écrire map(df, ~ sum(is.na(.))). Vous noterez le point utilisé ici pour le x de la fonction.

« Et si nous avons besoin d’itérer sur plusieurs vecteurs, que fait-on ? » Si la fonction prend plusieurs arguments (comme par exemple rnorm), vous pouvez faire appel à map2, qui prend en arguments deux listes et une fonction, sous la forme (.x, .y, .f, …). Ainsi, map2(list(2,3,4), list(5,6,7), rnorm) est l’équivalent à la rédaction de trois appels :

rnorm(n = 2, mean = 5)

rnorm(n = 3, mean = 6)

rnorm(n = 4, mean = 7)

Pretty cool, isn’t it? « Oui, mais si on a plus de deux arguments ? » Ne vous inquiétez pas, Hadley a pensé à tout : la fonction pmap vous permet d’itérer sur autant d’arguments que vous le souhaitez — vous trouverez ici une structure pmap(.l,.f), où .l est une liste des listes sur lesquelles itérer. Comme map, map2 et pmap dispose de ses équivalents avec _lgl, _int, _dbl, _chr et _df, pour un output de type similaire à l’input.

Enfin, si l’itération porte sur une liste de fonctions, plutôt que sur une liste d’arguments, faite appel à invoke_map(.f, .x), où .f est une liste de fonctions sur laquelle vous itérer avec l’argument x. Autrement dit, invoke_map(list(runif, rnorm), n = 5) est l’équivalent de runif(5) puis rnorm(5).

Vous connaissez maintenant les principales fonctions de purrr, l’outil incontournable pour écrire des scripts plus fluides et manipuler les données au format vecteur. Car oui, il en existe d’autres — pour les découvrir, n’hésitez pas à consulter la vignette

En utilisant ce package, gardez en tête qu’il a été conçu pour fonctionner avec le pipe %>% pour encore plus de lisibilité. Et pour en savoir encore plus, sur la rédaction de scripts et de fonctions, appelez-nous, nous pourrons vous en dire encore plus !


À propos de l'auteur

Colin Fay

Colin Fay

Data scientist & R Hacker


Commentaires


À lire également