class: center, middle .fl.w-40.pa2[ ![](https://raw.githubusercontent.com/MaelTheuliere/ateliers_rpackage/main/slides/www/packagescompagnons.png)<!-- --> ] .fl.w-60.pa2[ .f3[Créer son premier] .yellow.f3[package R] .f1[Ajouter] .yellow.f1[une fonction] .f1[dans votre package] ] .tr[ .f4[Juliette ENGELAERE-LEFEBVRE - Maël THEULIERE] ] --- # Objectif de cet atelier Après cet atelier vous saurez ajouter une fonction dans un package. C’est à dire que vous aurez compris : - ce qu’est une fonction ; - comment ajouter une fonction dans un package ; - comment documenter une fonction ; - comment tester une fonction. --- class: inverse, center, middle # Qu'est ce qu'une fonction ? --- # Qu'est ce qu'une fonction ? Une fonction est un objet de R. C'est une opération qui prend en entrée des arguments pour produire un résultat. Par exemple : - `abs()` prend comme argument un vecteur de nombre et produit un vecteur de nombre contenant la valeur absolue des nombres en argument. - `select()` de `{dplyr}` prend comme argument un dataframe et une liste de colonnes et produit en sortie un dataframe restreint à ces colonnes. - `write.csv()` prend en argument un dataframe, un lien vers un fichier, et produit en sortie un fichier csv contenant le dataframe, à l'endroit spécifié par le lien. --- # Définir une fonction Une fonction classique dans R se définie de la sorte : ```r ma_fonction <- function(a = 2, b = 1){ resultat <- a + 2*b return(resultat) } ``` L'instruction `function()` créer une fonction ici appelée `ma_fonction()`. Elle prend en arguments les paramètres de notre fonction, ici `a` et `b` auxquels ont peut assigner des valeurs par défaut, ici `2` et `1`. L'intérieur de nos accolades `{}` va définir le résultat produit par notre fonction. Ce résultat doit être retourné par l'instruction `return()`. Voilà comment se définit une fonction type qui produit en retour un objet R. Certaines fonctions ne produisent pas des objets R mais des instructions, comme par exemple `write.csv()` vu précédemment. --- # Les bonnes pratiques Pour créer une bonne fonction, il faut bien penser sa cohérence dans le workflow dans lequelle elle va s'inscrire : - Pour faire telle opération, dois je créer une fonction ou deux car un résultat intermédiaire pourrait m'intéresser ailleurs ? - Quels paramètres ? - Quelle complémentarité avec les fonctions existantes ? - Quelle convention de nommage ? Ensuite cette fonction devra être correctement documentée et testée. On verra dans la suite ce qu'est un test. --- class: inverse, center, middle # Ajouter une fonction dans votre package --- #### Ajouter une fonction dans votre package # créer le fichier .R .pull-left[ Pour rappel, le code d'une fonction doit être rajouté dans un script R du sous répertoire `R/`. Pour rajouter une fonction dans votre package, `{usethis}` vous facilite le travail : `usethis::use_r("ma_fonction")` va créer un fichier `ma_fonction.R` dans votre répertoire `R/`. ] .pull-right[ ![](www/ma_fonction.png) ] --- #### Ajouter une fonction dans votre package # Ajouter votre fonction dans le fichier .pull-left[ Ici on crée la fonction `ma_fonction` qui prend en paramètres : - un dataframe `data`, - deux nombres `n_head` et `n_tail`, et produit en sortie un dataframe contenant le début et la fin du dataframe `data`, en gardant `n_head` lignes du début et `n_tail` lignes de la fin. ⚠️ Dans une fonction, il est commun d'utiliser des fonctions d'autres packages. Dans ce cas, appelez-les en utilisant la convention `packages::fonction()`. ] .pull-right[ ```r ma_fonction <- function(data = NULL, n_head = 3, n_tail = 3){ res <- rbind(dplyr::slice_head(data, n = n_head), dplyr::slice_tail(data, n = n_tail) ) return(res) } ``` ] --- #### Ajouter une fonction dans votre package # Utiliser votre fonction `devtools::load_all()` vous permet de charger le contenu du package sur lequel vous travaillez, comme si vous l'aviez installé. Dans votre workflow habituel, vous allez utiliser souvent cette fonction pour tester les fonctions que vous ajoutez. ```r devtools::load_all() ``` Vous pouvez ensuite constater que votre fonction marche correctement 🎉 ```r ma_fonction(iris, 2, 3) ``` ``` ## Sepal.Length Sepal.Width Petal.Length Petal.Width Species ## 1 5.1 3.5 1.4 0.2 setosa ## 2 4.9 3.0 1.4 0.2 setosa ## 3 6.5 3.0 5.2 2.0 virginica ## 4 6.2 3.4 5.4 2.3 virginica ## 5 5.9 3.0 5.1 1.8 virginica ``` --- class: inverse, center, middle # Documenter votre fonction --- #### Documenter votre fonction # {roxygen2} Le package `{roxygen2}` va vous permettre de documenter votre fonction afin qu'une aide soit accessible pour celle-ci. --- #### Documenter votre fonction # {roxygen2} : créer un canevas .pull-left[ Pour ajouter une documentation, mettez le pointeur sur la fonction dans son script et utiliser le raccourci clavier `Ctrl + Alt + Shift + R` ou utiliser l'interface de Rstudio en cliquant sur `Code Tools`. ] .pull-right[ ![](www/ma_fonction_roxygen2.png) ] --- #### Documenter votre fonction # {roxygen2} : créer un canevas .pull-left[ Pour ajouter une documentation, mettez le pointeur sur la fonction dans son script et utiliser le raccourci clavier `Ctrl + Alt + Shift + R` ou utilisez l'interface de Rstudio en cliquant sur `Code Tools`. Une fois activé, roxygen2 vous rajoute un canevas de documentation. ] .pull-right[ ![](www/ma_fonction_roxygen2_caneva.png) ] --- #### Documenter votre fonction # {roxygen2} : compléter votre documentation .pull-left[ Vous n'avez plus qu'à compléter 🎉! ] .pull-right[ ```r #' Garder les lignes de début et de fin d'un dataframe #' #' @param data un dataframe #' @param n_head le nombre de lignes à garder du début du fichier #' @param n_tail le nombre de lignes à garder de la fin du fichier #' #' @return un dataframe #' @export #' #' @examples #' ma_fonction(mpg,3,3) ma_fonction <- function(data = NULL, n_head = 3, n_tail = 3){ res <- rbind(dplyr::slice_head(data, n = n_head), dplyr::slice_tail(data, n = n_tail) ) return(res) } ``` ] --- #### Documenter votre fonction # {roxygen2} : gestion des dépendances .pull-left[ `{roxygen2}` permet non seulement de gérer la documentation mais aussi les dépendances et les exports de notre package. Cela se traduit par l'alimentation du fichier `NAMESPACE`. La balise `@importFrom` permet de préciser les fonctions qu'on utilise dans le package. Cet ajout permettra de compléter le fichier `NAMESPACE` avec les dépendances de notre package. On ajoute une balise `@importFrom` pour chaque package utilisé. ] .pull-right[ ```r #' Garder les lignes de début et de fin d'un dataframe #' #' @param data un dataframe #' @param n_head le nombre de lignes à garder du début du fichier #' @param n_tail le nombre de lignes à garder de la fin du fichier #' #' @return un dataframe #' @importFrom dplyr slice_head slice_tail #' @export #' #' @examples #' ma_fonction(mpg,3,3) ma_fonction <- function(data = NULL, n_head = 3, n_tail = 3){ res <- rbind(dplyr::slice_head(data, n = n_head), dplyr::slice_tail(data, n = n_tail) ) return(res) } ``` ] --- #### Documenter votre fonction # {roxygen2} : gestion des exports .pull-left[ La balise `@export` permet aussi de compléter le fichier `NAMESPACE` en lui précisant cette fois ci que `ma_fonction()` est une fonction *exportée* de `{monpackage}`. Si cette balise n'est pas ajoutée, dans ce cas, la fonction restera purement interne au package. Cela est une convention utile pour définir des fonctions nécessaires à d'autres fonctions du package mais pas directement utiles pour les utilisateurs. ] .pull-right[ ```r #' Garder les lignes de début et de fin d'un dataframe #' #' @param data un dataframe #' @param n_head le nombre de lignes à garder du début du fichier #' @param n_tail le nombre de lignes à garder de la fin du fichier #' #' @return un dataframe #' @importFrom dplyr slice_head slice_tail #' @export #' #' @examples #' ma_fonction(mpg,3,3) ma_fonction <- function(data = NULL, n_head = 3, n_tail = 3){ res <- rbind(dplyr::slice_head(data, n = n_head), dplyr::slice_tail(data, n = n_tail) ) return(res) } ``` ] --- #### Documenter votre fonction # Des actuces avec `{prefixer}` ![](www/addin_prefixer_logo.png) .pull-left[ Le package `{prefixer}` permet de finaliser votre fonction et produire la documentation plus facilement. Il s'installe via : ```r remotes::install_github("dreamRs/prefixer") ``` Il s'agit d'un package qui installe un addin, c'est à dire qui ajoute des fonctionnalités à RStudio. L'objectif des addins est généralement d'accélérer la réalisation de tâches répétitives ou fastidieuses. ] .pull-right[ On accède aux fonctionnalités nouvelles via le menu addins : ![](www/addin_prefixer.png) On peut ajouter des raccourcis clavier pour accélérer encore l'utilisation des fonctions des addins. ] --- #### Documenter votre fonction - Astuces `{prefixer}` # Commande addin `Prefixer::` La commande `Prefixer::` ouvre une boite de dialogue qui vous propose d'ajouter, pour chaque fonction utilisée dans notre script de définition de fonction, le préfixe adéquat. ⚠️ Seuls les packages actuellement actifs (appelés via `library`) seront proposés. ![](www/addin_prefixer_fonct.png) La commande `Unprefix` supprime tous les préfixes de notre script actif. --- #### Documenter votre fonction - Astuces `{prefixer}` # Commande addin `@importFrom` .pull-left[ La commande `@importFrom` : - parcourt votre script de définition de fonction, - y détecte toutes les préfixes utilisés et - ajoute au dessus de la fonction, la ou les balises `@importFrom package1 fonctions1` ] .pull-right[ ![](www/addin_prefixer_importFrom.png) ] ⚠️ Ces lignes restent à adapter : - il faut préfixer les datasets, mais ne pas les faire figurer dans une balise importFrom qui est réservée aux fonctions, - on n'importe pas les autres fonctions du package en cours de développement, - le pronom `.data` n'est pas préfixé : ajouter `importFrom rlang .data` quand on y recourt. --- #### Documenter votre fonction - Astuces `{prefixer}` # Commande addin `Not-ASCII` Utiliser un encodage multi-plateforme est absolument nécessaire pour que notre package puisse fonctionner partout, que ce soit sur un serveur linux, un PC, un Mac... .pull-left[ La commande `@Not-ASCII` : - scanne l'ensemble du fichier contenant le script de définition de fonction, - y détecte tous les caractères à problème (entre "quote"), - et les convertit avec leur code unicode. ] .pull-right[ Par exemple : ```r filter(dataset, TypeZone == "Régions") ``` devient : ```r filter(dataset, TypeZone == "R\u00e9gions") ``` ] Cela ne fonctionne pas dans les commentaires de documentation, de toutes façons, ils ne seraient rendus correctement. --- #### Documenter votre fonction # {roxygen2} : document() .pull-left[ Une fois votre documentation effectuée, la fonction `devtools::document()` va exploiter ces balises en 1. créant le fichier de documentation de votre fonction, `ma_fonction.Rd`, dans le répertoire `man/` et 2. en mettant à jour le fichier NAMESPACE. `devtools::check()` intègre `devtools::document()` donc vous aurez au départ rarement à utiliser `devtools::document()` de façon isolée. ] .pull-right[ ```r > devtools::document() Updating monpackage documentation Loading monpackage Writing NAMESPACE Writing NAMESPACE Writing ma_fonction.Rd ``` ] --- #### Documenter votre fonction # {roxygen2} : document() .pull-left[ Une fois votre documentation effectuée, la fonction `devtools::document()` va exploiter ces balises en 1. créant le fichier de documentation de votre fonction, **`ma_fonction.Rd`**, dans le répertoire `man/` et 2. en mettant à jour le fichier NAMESPACE. `devtools::check()` intègre `devtools::document()` donc vous aurez au départ rarement à utiliser `devtools::document()` de façon isolée. ] .pull-right[ ![](www/ma_fonction_rd.png) ] --- #### Documenter votre fonction # {roxygen2} : document() .pull-left[ Une fois votre documentation effectuée, la fonction `devtools::document()` va exploiter ces balises en 1. créant le fichier de documentation de votre fonction, `ma_fonction.Rd`, dans le répertoire `man/` et 2. en mettant à jour le fichier **NAMESPACE**. `devtools::check()` intègre `devtools::document()` donc vous aurez au départ rarement à utiliser `devtools::document()` de façon isolée. ] .pull-right[ ![](www/namespace.png) ] --- #### Documenter votre fonction # Partie 'imports' de DESCRIPTION Il reste une dernière chose à faire pour que les dépendances de notre package soient traitées correctement : déclarer le ou les packages dont dépend notre fonction au niveau du fichier DESCRIPTION. Cela se fait notamment avec l'instruction `usethis::use_package("nomdupackage")` à consigner dans le dev_history.R. Dans le cas de notre exemple : `usethis::use_package("dplyr")` .pull-left[ ![](www/use_package_devhist.png) ] .pull-right[ ```r > usethis::use_package("dplyr") √ Setting active project to 'C:/Users/juliette.engelaere/Documents/Travail/R_local/monpremierpackage' * Refer to functions with `dplyr::fun()` ``` ] --- #### Documenter votre fonction # Partie 'imports' de DESCRIPTION L'instruction `usethis::use_package` va compléter notre fichier DESCRIPTION au niveau de la partie 'imports' : ![](www/use_package_description.png) Le fichier DESCRIPTION peut être complété à la main. --- #### Documenter votre fonction # Partie 'imports' de DESCRIPTION Dans le cas de dépendance à des packages **qui ne sont pas hébergés par le CRAN** (par exemple COGiter), il faut le préciser. Sans ça, la gestion de cette dépendance ne sera pas traitée correctement par R lors de l'installation du package par l'utilisateur. .pull-left[ Cela se fait par exemple avec : ```r > usethis::use_dev_package("COGiter", type = "Imports", remote = "gitlab::dreal-datalab/cogiter") √ Adding 'gitlab::dreal-datalab/cogiter' to Remotes field in DESCRIPTION * Refer to functions with `COGiter::fun()` ``` ] .pull-right[ Cela a pour effet d'ajouter une partie *'Remotes :'* à notre fichier description : ![](www/use_remote_package.png) ] --- class: inverse, center, middle # Tester votre fonction --- #### Tester votre fonction # Qu'est ce qu'un test ? Une fois une fonction ajoutée à votre package, vous allez créer un ou plusieurs tests la concernant. Un test définit un comportement attendu de votre fonction. Par exemple, on s'attend à ce que 2+2 soit égal à 4. Plus généralement on s'attend à ce qu'une addition renvoie un nombre. Sur notre exemple de fonction, le résultat de `ma_fonction()` doit être un *dataframe*. On va pouvoir écrire un test qui cherche à vérifier cela sur un exemple particulier. --- #### Tester votre fonction # Pourquoi faire un test ? Les tests permettent de sécuriser votre développement. Imaginez sur notre exemple que la définition de `slice_head()` et que par exemple le paramètre `n` change de nom et devient `nb`. Le fait d'avoir défini un test pour s'assurer sur un jeu d'exemples du résultats attendu vous permettra très vite d'identifier ce changement. --- #### Tester votre fonction # Comment faire un test ? - Réfléchir au comportement attendu de la fonction. Dans notre exemple, le résultat de notre fonction est un dataframe de `n_head` + `n_tail` lignes. .pull-left[ - Grâce à `{usethis}` : * initialiser les tests dans `dev_history.R`, * et créer le fichier du test de la fonction `ma_fonction()`. ] .pull-right[ On recourt pour cela à la librairie `{testthat}`. ```r usethis::use_testthat() usethis::use_test("ma_fonction") ``` ] Un répertoire `tests` dédié aux tests a été créé à la racine du projet. Il contient un premier script R, `testthat.R`, qui initie les tests de notre package, et un répertoire `testthat` qui contient les fichiers de tests de chaque fonction. --- #### Tester votre fonction # Comment faire un test ? .pull-left[ - Dans le fichier de test `test-ma_fonction.R` qui s'est ouvert, on exécute la fonction et on vérifie que le résultat a les propriétés attendues. ] .pull-right[ ```r objet <- ma_fonction(data = iris, n_head = 3, n_tail = 3) test_that("ma_fonction() renvoie un dataframe", { expect_is(objet, "data.frame") }) test_that("ma_fonction() renvoie le bon nombre de lignes", { expect_equal(nrow(objet), 6) }) ``` ] Lors d'une prochaine vérification de notre package, avec `devtools::check()`, le test sera automatiquement exécuté.