forked from introRcpp/introRcpp.github.io
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy path03-packages.Rmd
255 lines (211 loc) · 10.2 KB
/
03-packages.Rmd
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
```{R echo = FALSE, warnings = FALSE}
source("include.cpp.r")
includeCppPath <- "../introRcppPackages/src/"
library(introRcppBases) # pour avoir la fonction squareRoot !
```
# Création d'un package R
Pour diffuser votre travail, ou pour ne pas avoir besoin de recompiler vos
fonctions à chaque fois, il faut créer un package R. `Rstudio` est d'une grande
aide pour cela. Nous allons dans ce chapitre faire les premiers pas dans la
création d'un package ; le tome du manuel de R intitulé *Writing R extensions*
reste une référence indispensable.
Le package que nous allons créer s'appelle `introRcppPackages`.
Vous pouvez le retrouver à l'adresse `https://github.com/introRcpp/introRcppPackages`.
## Créer l'arborescence de fichiers
Pour commencer sélectionner le menu `File` l'option `New Project`, puis `New Directory`,
puis `R package using Rcpp`. Vous allez pouvoir choisir à quel endroit le nouveau
répertoire qui va contenir votre package (et portera son nom) sera installé. Nous
choisissons d'appeler notre package `introRcppPackages`.
Le répertoire `introRcppPackages/` contient:
* les fichiers `DESCRIPTION` et `NAMESPACE`
* les répertoires `R/`, `src/` et `man/` dont nous allons parler plus bas
* des fichiers qui ne font pas partie du package :
+ un fichier `Read-and-delete-me` (obtempérez)
+ un fichier `introRcppPackages.Rproj` et un dossier (caché) `.Rproj.user/` qui sont utilisés par `Rstudio`
+ un fichier `.Rbuilbignore` qui contient des expressions régulières destinées à
informer R de la présence de fichiers qui ne font pas partie du package...
Le contenu de `DESCRIPTION` est assez clair -- vous pouvez et devez le modifier:
```
Package: introRcppPackages
Type: Package
Title: What the Package Does in One 'Title Case' Line
Version: 1.0
Date: 2020-03-24
Author: Your Name
Maintainer: Your Name <[email protected]>
Description: One paragraph description of what the package does as one or more full
sentences.
License: GPL (>= 2)
Imports: Rcpp (>= 1.0.3)
LinkingTo: Rcpp
```
Il est possible d'inclure des informations supplémentaires, par exemple un champ `Encoding` pour
spécifier la façon dont les éventuelles lettres accentuées sont encodées (`latin1` et `UTF-8` sont les solutions les plus fréquentes).
J’insère pour ma part la ligne
```
Encoding: UTF-8
```
qui correspond à l’encodage par défaut sous Linux et Mac OS et me permet d'accentuer correctement mon prénom dans le champ `Author`. Les utilisateurs de Windows choisiront peut-être plus commodément l'encodage `latin1`, mais Rstudio peut gérer l'encodage de votre choix et vous demande de choisir lors de la première sauvegarde d'un fichier.
Le fichier `NAMESPACE` contient deux lignes importantes pour l'utilisation de fonctions écrites avec Rcpp:
```
useDynLib(introRcppPackages, .registration=TRUE)
importFrom(Rcpp, evalCpp)
```
La ligne
```
exportPattern("^[[:alpha:]]+")
```
dit à R que toutes les fonctions dont le nom commence par un caractère alphanumérique sont exportées du package. C’est très bien quand on ne développe que pour soi, pour un package destiné à la diffusion il est souvent nécessaire de modifier cela. Nous le ferons plus tard.
## Inclure une fonction C++
Les fonctions C++ sont dans le répertoire `src/`.
Il contient deux fichiers, `rcpp_hello_world.cpp` qui contient quelques exemples basiques ; et `RcppExports.cpp`,
qui est généré par la fonction `Rcpp::compileAttributes()`. Dans un premier temps vous
pouvez ignorer son contenu.
Créons un fichier `squareRoot.cpp`, contenant
```{R echo = FALSE, results = "asis"}
include.cpp('squareRoot.cpp', FALSE)
```
Le nom du fichier n'a pas besoin de coincider avec celui de la fonction, c'est juste plus commode pour s'y retrouver.
Vous pouvez maintenant cliquer sur `Install and Restart` (sous l’onglet `Build`). Rstudio compile le package et relance la session R, puis charge le package. Vous pouvez tester la fonction `squareRoot` !
```{r cache = TRUE}
library(introRcppPackages)
squareRoot(123)
```
## Ce que Rstudio a fait avant la compilation
Rstudio a appelé la fonction `Rcpp::compileAttributes()` qui a modifié le fichier `RcppExports.cpp`. Elle a créé cette fonction
```{Rcpp eval = FALSE}
RcppExport SEXP _introRcppPackages_squareRoot(SEXP xSEXP) {
BEGIN_RCPP
Rcpp::RObject rcpp_result_gen;
Rcpp::RNGScope rcpp_rngScope_gen;
Rcpp::traits::input_parameter< double >::type x(xSEXP);
rcpp_result_gen = Rcpp::wrap(squareRoot(x));
return rcpp_result_gen;
END_RCPP
}
```
qui est en fait celle qui est appelée par R. Comment cela ? La fonction R qui correspond est dans le fichier `R/RcppExports.R`:
```{R eval = FALSE}
squareRoot <- function(x) {
.Call(`_introRcppPackages_squareRoot`, x)
}
```
## Inclure une fonction R
La fonction `squareRoot` n'est pas totalement satisfaisante. Une bonne idée serait de
vérifier -- dans le code R -- que l'utilisateur a bien passé un unique élément de type double. On peut
créer dans le répertoire `R/` un fichier qu'on appelera par exemple `square.root.r` et qui contient
```{r eval = FALSE}
square.root <- function(x) {
if( typeof(x) != "double" )
stop("This function works on doubles")
if( length(x) != 1 )
stop("This function works on single numbers")
squareRoot(x)
}
```
## Contrôler quelles sont les fonctions exportées
Puisqu'on a créé `square.root`, on ne veut pas que l'utilisateur puisse utiliser `squareRoot`.
On va donc modifier notre `NAMESPACE` pour qu'il contienne
```
useDynLib(introRcppPackages, .registration=TRUE)
importFrom(Rcpp,evalCpp)
export(square.root)
```
Ainsi la seule fonction exportée par notre package est `square.root`. On peut toujours, à nos risques et périls,
utiliser la fonction non exportée avec la syntaxe `introRcppPackages:::squareRoot` (notez le triple deux-points).
## Paramétrer la compilation
On peut inclure dans `src/` un fichier `Makevars` qui permet notamment de préciser quels flags
doivent être inclus dans la commande de compilation. Si on utilise du C++11, il faut notamment
inclure une ligne
```
PKG_CXXFLAGS = -std=c++11
```
sinon la compilation ne sera pas possible.
## Documenter les fonctions avec `roxygen2`
Les fichiers de documentation sont inclus dans le répertoire `man/`. Ce sont des fichiers en `.Rd`
qui peuvent être écrits à la main ; une solution qui s'avère à l'usage très commode (facilité
d’écriture d’une part, de maintenance du package d’autre part) est de les faire générer par
`roxygen2`.
Pour cela il faut tout d'abord installer ce package : `install.packages("roxygen2")`. Il
est possible que l’installation soit pénible car ce package dépend de `xml2` qui nécessite
d’installer d’autres composantes logicielles sur le système. Soyez attentifs aux messages d'erreur,
ils sont informatifs. Une solution simple sous un linux de type Ubuntu est d'utiliser le
gestionnaire de paquets pour installer `r-cran-xml2` ou `r-cran-roxygen2` !
Pour documenter la fonction `square.root`, placez le curseur dans cette fonction puis cliquez sur la
baguette magique et choisissez `Insert Roxygen Skeleton`. Votre fichier ressemble maintenant à ceci:
```{r eval = FALSE}
#' Title
#'
#' @param x
#'
#' @return
#' @export
#'
#' @examples
square.root <- function(x) {
if( typeof(x) != "double" )
stop("This function works on doubles")
if( length(x) != 1 )
stop("This function works on single numbers")
squareRoot(x)
}
```
On va compléter cette ébauche ainsi:
```{r eval = FALSE}
#' Computes a square root
#'
#' @param x a double vector of length 1
#'
#' @details This function is for pedagogical illustration only. Please use
#' `base::sqrt` in R or `sqrt` in C++.
#'
#' @return The square root of `x`
#' @export
#'
#' @examples
#' square.root(2)
#'
square.root <- function(x) {
if( typeof(x) != "double" )
stop("This function works on doubles")
if( length(x) != 1 )
stop("This function works on single numbers")
squareRoot(x)
}
```
Maintenant, on va faire générer à Rstudio le fichier `man/square.root.Rd` qui correspond. Pour
cela, il faut d'abord aller dans l'onglet `Build`, puis cliquer sur `More`, `Configure Build Tools`,
cocher la case `Generate documentation with Roxygen` (cocher au minimum la case `Rd files`).
Ensuite, cliquez sur `Build > More > Document` pour faire générer ce fichier. Comme notre page
d'aide contient des lettres accentuées, cela ne fonctionnera que si vous avez inséré dans
`DESCRIPTION` la ligne `Encoding: UTF-8`. Le message d'erreur en l’absence de ce champ n’est pas
très instructif, l'information se trouve dans un des warnings qui suit. En pratique, la
documentation est généralement écrite en anglais, et les lettres accentuées y sont presque toujours
absentes.
Une fois la documentation générée (regardez le contenu de `man/square.root.Rd`) vous pouvez
réinstaller le package et admirer la page de documentation en tapant `?square.root` et
`example(square.root)`.
![Documentation de la fonction `square.root`](manuel_square_root.png)
## Générer le fichier `NAMESPACE` avec `roxygen2`
Le tag `@export` de `roxygen2` signale que cette fonction est exportée. Cela permet à `roxygen2` de
générer, en plus de l'aide, le fichier `NAMESPACE` et nous évite d'insérer à la main des lignes
d'exportation comme `export(square.root)`. Cependant `roxygen2` refuse (sagement) d'effacer un
`NAMESPACE` qu'il n'a pas généré lui-même.
Pour y remédier il semble qu'il n'y ait pas de meilleure solution que d'insérer au début du fichier
`NAMESPACE` la ligne suivante, qui permet à `roxygen2` de considérer que le `NAMESPACE` peut être
effacé:
```
# Generated by roxygen2
```
Il existe sûrement une solution plus propre. Un problème subsiste cependant: `roxygen2` ne génère
pas les deux lignes indispensables au fonctionnement d'un package avec `Rcpp`, que nous avons
mentionnées plus haut. La solution est d'insérer dans le répertoire `R/` un fichier à cette fin.
Nous l'appellerons par exemple `zzz.r` et il contiendra les lignes suivantes:
```{r}
#' @useDynLib introRcppPackages, .registration=TRUE
#' @importFrom Rcpp evalCpp
NULL
```
Le `NULL` final peut être remplacé par un `0` ou ce que vous voulez (des appels aux fonctions
`.onLoad` et `.onAttach` par exemple), mais il faut qu'il y ait un objet R à évaluer sinon le
fichier n'est pas pris en compte par `roxygen2`.