-
Notifications
You must be signed in to change notification settings - Fork 19
/
14-fonctions.qmd
1336 lines (944 loc) · 38.6 KB
/
14-fonctions.qmd
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
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
# Écrire ses propres fonctions
{{< include _setup.qmd >}}
## Introduction et exemples
### Structure d'une fonction
Nous avons vu lors de l'introduction à R que le langage repose sur deux grands concepts : les *objets* et les *fonctions*. Pour reprendre une citation de John Chambers, en R, tout ce qui existe est un objet, et tout ce qui se passe est une fonction.
Le principe d'une fonction est de prendre en entrée un ou plusieurs arguments (ou paramètres), d'effectuer un certain nombre d'actions et de renvoyer un résultat :
![](resources/figures/schema_fonction)
Nous avons déjà rencontré et utilisé un grand nombre de fonctions, certaines assez simples (`mean`, `max`...) et d'autres beaucoup plus complexes (`summary`, `mutate`...). R, comme tout langage de programmation, offre la possibilité de créer et d'utiliser ses propres fonctions.
Voici un exemple de fonction très simple, quoi que d'une utilité douteuse, puisqu'elle se contente d'ajouter 2 à un nombre :
```{r}
ajoute2 <- function(x) {
res <- x + 2
return(res)
}
```
En exécutant ce code, on crée une nouvelle fonction nommée `ajoute2`, que l'on peut directement utiliser dans un script ou dans la console :
```{r}
ajoute2(3)
```
On va décomposer pas à pas la structure de cette première fonction.
D'abord, une fonction est créée en utilisant l'instruction `function`. Celle-ci est suivie d'une paire de parenthèses et d'une paire d'accolades.
```{r eval=FALSE}
function() {
}
```
Dans les parenthèses, on indique les *arguments* de la fonction, ceux qui devront lui être passés quand nous l'appellerons. Ici notre fonction ne prend qu'un seul argument, que nous avons décidé arbitrairement de nommer `x`.
```{r eval=FALSE}
function(x) {
}
```
Les accolades comprennent une série d'instructions R qui constituent le *corps* de notre fonction. C'est ce code qui sera exécuté quand notre fonction est appelée. On peut utiliser dans le corps de la fonction les arguments qui lui sont passés. Ici, la première ligne utilise la valeur de l'argument `x`, lui ajoute 2 et stocke le résultat dans un nouvel objet `res`.
```{r eval=FALSE}
function(x) {
res <- x + 2
}
```
Pour qu'elle soit utile, notre fonction doit renvoyer le résultat qu'elle a calculé précédemment. Ceci se fait via l'instruction `return` à qui on passe la valeur à retourner.
```{r eval=FALSE}
function(x) {
res <- x + 2
return(res)
}
```
Enfin, pour que notre fonction puisse être appelée et utilisée, nous devons lui donner un nom, ou plus précisément la stocker dans un objet. Ici on la stocke dans un objet nommé `ajoute2`.
```{r eval=FALSE}
ajoute2 <- function(x) {
res <- x + 2
return(res)
}
```
::: {.callout-note}
Les fonctions étant des objets comme les autres, elles suivent les mêmes contraintes pour leur nom : on a donc droit aux lettres, chiffres, point et tiret bas.
Attention à ne pas donner à votre fonction le nom d'une fonction déjà existante : par exemple, si vous créez une fonction nommée `table`, la fonction du même nom de R base ne sera plus disponible (sauf à la préfixer avec `base::table`). Si vous "écrasez" par erreur une fonction existante, il vous suffit de relancer votre session R et de trouver un nouveau nom.
:::
Avec le code précédent, on a donc créé un nouvel objet `ajoute2` de type `function`. Cette nouvelle fonction prend un seul argument `x`, calcule la valeur `x + 2` et retourne ce résultat. On l'utilise en tapant son nom suivi de la valeur de son argument entre parenthèses, par exemple :
```{r}
ajoute2(41)
```
Ou encore :
```{r}
y <- 5
z <- ajoute2(y)
z
```
À noter que comme `x + 2` fonctionne si `x` est un vecteur, on peut aussi appeler notre fonction en lui passant un vecteur en argument.
```{r}
vec <- 1:5
ajoute2(vec)
```
Si on récapitule, une fonction se définit donc de la manière suivante :
![](resources/figures/structure_fonction)
Une fonction peut évidemment prendre plusieurs arguments. Dans ce cas on liste les arguments dans les parenthèses en les séparant par des virgules :
```{r}
somme <- function(x, y) {
return(x + y)
}
```
```{r}
somme(3, 5)
```
Une fonction peut aussi n'accepter aucun argument, dans ce cas on laisse les parenthèses vides.
```{r}
miaule <- function() {
return("Miaou")
}
miaule()
```
À noter que si on appelle une fonction avec un nombre d'arguments incorrect, cela génère une erreur.
```{r error = TRUE}
somme(1)
```
```{r error = TRUE}
miaule("ouaf")
```
### Exemple de fonction
Prenons un exemple un peu plus élaboré : la fonction `table()` retourne le tri à plat en effectifs d'une variable qualitative. On souhaite créer une fonction qui calcule plutôt le tri à plat en pourcentages. Voici une manière de le faire :
```{r}
prop_tab <- function(v) {
tri <- table(v)
effectif_total <- length(v)
tri <- tri / effectif_total * 100
return(tri)
}
```
Notre fonction prend en entrée un argument nommé `v`, en l'occurrence un vecteur représentant une variable qualitative. On commence par faire le tri à plat de ce vecteur avec `table`, puis on calcule la répartition en pourcentages en divisant ce tri à plat par l'effectif total et en multipliant par 100.
Testons avec un vecteur d'exemple :
```{r}
vec <- c("rouge", "vert", "vert", "bleu", "rouge")
prop_tab(vec)
```
Testons sur une variable du jeu de données `hdv2003`^[Le jeu de données `hdv2003` fait partie de l'extension `questionr`, il est décrit @sec-hdv2003.] :
```{r}
library(questionr)
data(hdv2003)
prop_tab(hdv2003$qualif)
```
Ça fonctionne, mais avec une petite limite : par défaut `table()` ignore les `NA`. On peut modifier ce comportement en lui ajoutant un argument `useNA = "ifany"`.
```{r}
prop_tab <- function(v) {
tri <- table(v, useNA = "ifany")
effectif_total <- length(v)
tri <- tri / effectif_total * 100
return(tri)
}
prop_tab(hdv2003$qualif)
```
::: {.callout-warning}
Quand on modifie une fonction existante, il faut exécuter à nouveau le code correspondant à sa définition pour la "mettre à jour". Ici, si on ne le fait pas l'objet `prop_tab` contiendra toujours l'ancienne définition.
Pour "mettre à jour" une fonction après avoir modifié son code, on peut soit sélectionner le code qui la définit et l'exécuter de la manière habituelle, soit, dans RStudio, se positionner dans le corps de la fonction et utiliser le raccourci clavier `Ctrl + Alt + F`.
:::
Autre amélioration possible : on pourrait vouloir modifier le nombre de décimales affichées pour les pourcentages, par exemple en les limitant à 1. Pour cela on ajoute une instruction `round()`.
```{r}
prop_tab <- function(v) {
tri <- table(v, useNA = "ifany")
effectif_total <- length(v)
tri <- tri / effectif_total * 100
tri <- round(tri, 1)
return(tri)
}
prop_tab(hdv2003$qualif)
```
Ça fonctionne ! Cela dit, limiter à un chiffre après la virgule ne convient pas forcément dans tous les cas. L'idéal serait d'offrir la possibilité à la personne qui appelle la fonction de choisir elle-même la précision de l'affichage. Comment ? Tout simplement en ajoutant un deuxième argument à notre fonction, que nous nommerons `decimales`, et en utilisant cet argument à la place du 1 dans l'appel à `round()`.
```{r}
prop_tab <- function(v, decimales) {
tri <- table(v, useNA = "ifany")
effectif_total <- length(v)
tri <- tri / effectif_total * 100
tri <- round(tri, decimales)
return(tri)
}
```
Désormais, notre fonction s'utilise en lui indiquant deux arguments :
```{r}
prop_tab(hdv2003$qualif, 1)
```
De la même manière, on pourrait vouloir laisser le choix à l'utilisateur d'afficher ou non les `NA` dans le tri à plat. C'est possible en ajoutant un troisième argument à notre fonction et en utilisant sa valeur dans le paramètre `useNA` de `table()`.
```{r}
prop_tab <- function(v, decimales, useNA) {
tri <- table(v, useNA = useNA)
effectif_total <- length(v)
tri <- tri / effectif_total * 100
tri <- round(tri, decimales)
return(tri)
}
prop_tab(hdv2003$qualif, 1, "no")
```
### Effets de bord et affichage de messages
Parfois une fonction n'a pas pour objectif de renvoyer un résultat mais d'accomplir une action, comme générer un graphique, afficher un message, enregistrer un fichier... Dans ce cas la fonction peut ne pas inclure d'instruction `return()`.
::: {.callout-note}
Les actions "visibles" dans notre session R accomplies par une fonction en-dehors du résultat renvoyé sont appelés des *effets de bord*.
:::
Par exemple la fonction suivante prend en argument un vecteur et génère un diagramme en barres du tri à plat de cette variable (en modifiant un peu la présentation au passage).
```{r}
my_barplot <- function(var) {
tri <- table(var)
barplot(tri, col = "skyblue", border = NA)
}
my_barplot(hdv2003$clso)
```
Un autre effet de bord très courant consiste à afficher des informations dans la console. Pour cela on peut utiliser `print`, qui affiche de manière aussi lisible que possible l'objet qu'on lui passe en argument :
```{r}
indicateurs <- function(v) {
print(mean(v))
print(sd(v))
}
indicateurs(hdv2003$age)
```
Quand on souhaite seulement afficher une chaîne de caractère, on peut utiliser `cat()` qui fournit une sortie plus lisible que `print` :
```{r}
hello <- function(nom) {
cat("Bonjour,", nom, "!")
}
hello("Pierre-Edmond")
```
Enfin, on peut aussi utiliser `message()` qui, comme son nom l'indique, affiche un message dans la console, avec une mise en forme spécifique. En général on l'utilise plutôt pour afficher des informations relatives au déroulement de la fonction.
Dans l'exemple suivant, on utilise la fonction `runif()` pour générer aléatoirement `n` nombres entre 0 et 1 et on affiche avec `cat()` la valeur du plus petit nombre généré. Comme l'exécution du `runif()` peut prendre du temps si `n` est grand, on affiche un message avec `message()` pour prévenir l'utilisateur.
```{r}
min_alea <- function(n) {
message("Génération de ", n, " nombres aléatoires...")
v <- runif(n)
cat("Le plus petit nombre généré vaut", min(v))
}
min_alea(50000)
```
### Utilité des fonctions
On peut se demander dans quels cas il est utile de créer une fonction.
Une règle courante considère que dès qu'on a répété le même code plus de deux fois, il est préférable d'en faire une fonction. Celles-ci ont en effet comme avantage d'éviter la duplication du code.
Imaginons que nous avons récupéré un jeu de données avec toute une série de variables ayant les modalités `"1"` et `"2"` qui correspondent aux réponses `"Oui"` et `"Non"` à des questions. On crée un *data frame* fictif comportant quatre variables de ce type :
```{r}
df <- data.frame(
q1 = c("1", "1", "2", "1"),
q2 = c("1", "2", "2", "2"),
q3 = c("2", "2", "1", "1"),
q4 = c("1", "2", "1", "1")
)
df
```
On a vu @sec-recoder-une-variable-qualitative qu'on peut recoder l'une de ces variables à l'aide de la fonction `fct_recode()` de l'extension `forcats` :
```{r, eval = FALSE}
df$q1 <- fct_recode(df$q1,
"Oui" = "1",
"Non" = "2"
)
```
On peut donc être tenté de dupliquer ce code autant de fois qu'on a de questions à recoder :
```{r, eval = FALSE}
df$q1 <- fct_recode(df$q1,
"Oui" = "1",
"Non" = "2"
)
df$q2 <- fct_recode(df$q2,
"Oui" = "1",
"Non" = "2"
)
df$q3 <- fct_recode(df$q3,
"Oui" = "1",
"Non" = "2"
)
df$q4 <- fct_recode(df$q4,
"Oui" = "1",
"Non" = "2"
)
```
Mais il est plus judicieux dans ce cas de créer une fonction pour ce recodage :
```{r}
recode_oui_non <- function(var) {
var_recodee <- fct_recode(var,
"Oui" = "1",
"Non" = "2"
)
return(var_recodee)
}
```
En effet, il est alors très simple d'appliquer ce recodage à plusieurs variables :
```{r, eval = FALSE}
df$q1 <- recode_oui_non(df$q1)
df$q2 <- recode_oui_non(df$q2)
df$q3 <- recode_oui_non(df$q3)
df$q4 <- recode_oui_non(df$q4)
```
Autre avantage, si on réalise qu'on a commis une erreur et qu'en fait le code `"1"` correspondait à `"Non"` et le code `"2"` à `"Oui"`, on n'a pas besoin de modifier tous les endroits où on a copié/collé notre recodage : on a juste à corriger la définition de la fonction.
Les avantages de procéder ainsi sont donc multiples :
- créer une fonction évite la répétition du code et le rend moins long et plus lisible, surtout si on donne à notre fonction un nom explicite permettant de comprendre facilement ce qu'elle fait.
- créer une fonction évite les erreurs de copier/coller du code.
- une fonction permet de mettre à jour plus facilement son code : si on se rend compte d'une erreur ou si on souhaite améliorer son fonctionnement, on n'a qu'un seul endroit à modifier.
- enfin, créer des fonctions permet potentiellement de rendre son code réutilisable d'un script à l'autre ou même d'un projet à l'autre. Voire, à terme, de les regrouper dans un *package* pour soi-même ou pour diffusion à d'autres utilisateurs et utilisatrices de R.
## Arguments et résultat d'une fonction
### Définition des arguments
Les arguments (ou paramètres) d'une fonction sont ce qu'on lui donne "en entrée", et qui vont soit lui fournir des données, soit modifier son comportement. La liste des arguments acceptés par une fonction est indiquée entre les parenthèses de l'appel de `function()` :
```{r, eval = FALSE}
ma_fonction <- function(arg1, arg2, arg3) {
print(arg1)
print(arg2)
print(arg3)
}
```
::: {.callout-note}
Une fonction peut aussi ne pas accepter d'arguments, dans ce cas on la définit juste avec `function()`.
:::
Lors de l'appel de la fonction, on peut lui passer les arguments *par position* :
```{r, eval = FALSE}
ma_fonction(x, 12, TRUE)
```
Dans ce cas, `arg1` vaudra `x`, `arg2` vaudra `12` et `arg3` vaudra `TRUE`.
On peut aussi passer les arguments *par nom* :
```{r, eval = FALSE}
ma_fonction(arg1 = x, arg2 = 12, arg3 = TRUE)
```
Quand on passe les arguments par nom, on peut les indiquer dans l'ordre que l'on souhaite :
```{r, eval = FALSE}
ma_fonction(arg1 = x, arg3 = TRUE, arg2 = 12)
```
Et on peut évidemment mélanger passage par position et passage par nom :
```{r, eval = FALSE}
ma_fonction(x, 12, arg3 = TRUE)
```
Le plus souvent, les premiers arguments acceptés par une fonction sont les données sur lesquelles elle va travailler, tandis que les arguments suivants sont des paramètres qui vont modifier son comportement. Par exemple, `median` accepte comme premier argument `x`, un vecteur, puis un argument `na.rm` qui va changer sa manière de calculer la médiane des valeurs de `x`.
::: {.callout-note}
En général on appelle la fonction en passant les paramètres correspondant aux données par position, et les autres en les nommant. C'est ainsi qu'on ne fait ni `median(x = tailles, na.rm = TRUE)` ni `median(tailles, TRUE)`, mais plutôt `median(tailles, na.rm = TRUE)`.
En ce qui concerne le nom des arguments, en général ceux correspondant aux données transmises à une fonction peuvent avoir des noms relativement génériques (`x`, `y`, `v` pour un vecteur, `data` ou `df` pour un data.frame...). Les autres doivent par contre avoir des noms à la fois courts et explicites : par exemple plutôt `decimales` que `nd` ou `nombre_de_decimales`.
:::
### Valeurs par défaut
Au moment de la définition de la fonction, on peut indiquer une valeur par défaut qui sera prise par l'argument si la personne qui utilise la fonction n'en fournit pas.
Si on reprend la fonction `prop_tab` déjà définie plus haut :
```{r, eval = FALSE}
prop_tab <- function(v, decimales, useNA) {
tri <- table(v, useNA = useNA)
tri <- tri / length(v) * 100
tri <- round(tri, decimales)
return(tri)
}
```
On peut indiquer une valeur par défaut aux arguments `decimales` et `useNA` de la manière suivante :
```{r}
prop_tab <- function(v, decimales = 1, useNA = "ifany") {
tri <- table(v, useNA = useNA)
tri <- tri / length(v) * 100
tri <- round(tri, decimales)
return(tri)
}
```
Si on appelle `prop_tab` en lui passant uniquement le vecteur `v`, on voit que `decimales` vaut bien 1 et `useNA` vaut bien `"ifany":
```{r}
prop_tab(hdv2003$qualif)
```
### Arguments obligatoires et arguments facultatifs {#sec-arg-oblig}
Si un argument n'a pas de valeur par défaut, il est *obligatoire* : si l'utilisateur essaye d'appeler la fonction sans définir cet argument, cela génère une erreur.
```{r error=TRUE}
prop_tab <- function(v, decimales, useNA) {
tri <- table(v, useNA = useNA)
tri <- tri / length(v) * 100
tri <- round(tri, decimales)
return(tri)
}
prop_tab(hdv2003$sexe)
```
::: {.callout-note}
Pour être tout à fait précis, l'erreur est générée uniquement lorsque l'argument sans valeur par défaut est utilisé dans la fonction.
:::
Si à l'inverse un argument a une valeur par défaut, il devient *facultatif* : on peut appeler la fonction sans le définir.
```{r}
prop_tab <- function(v, decimales = 1, useNA = "ifany") {
tri <- table(v, useNA = useNA)
tri <- tri / length(v) * 100
tri <- round(tri, decimales)
return(tri)
}
prop_tab(hdv2003$sexe)
```
Parfois un argument est facultatif mais on n'a pas forcément de valeur par défaut à lui attribuer. Dans ce cas on lui attribue en général par défaut la valeur `NULL`, et on utilise l'instruction `if()` dans la fonction pour tester s'il a été défini ou pas. Ce cas de figure est détaillé @sec-test-arg.
### L'argument `...`
Une fonction peut prendre un argument spécial nommé `...` :
```{r}
ma_fonction <- function(x, correct = TRUE, ...) {
}
```
Cet argument spécial "capture" tous les arguments présents et qui n'ont pas été définis avec la fonction. Par exemple, si on appelle la fonction précédente avec :
```{r eval=FALSE}
ma_fonction(1:5, correct = FALSE, title = "Titre", size = 12)
```
Alors `...` contiendra les arguments `title` et `size` et leurs valeurs.
::: {.callout-note}
Si on veut accéder à la valeur de `size` dans `...`, on utilise `list(...)$size`.
:::
En général `...` est utilisé pour passer ces arguments à d'autres fonctions. Reprenons notre fonction `my_barplot` définie précédemment :
```{r}
my_barplot <- function(var) {
tri <- table(var)
barplot(tri, col = "skyblue", border = NA)
}
```
On pourrait permettre de personnaliser les couleurs des barres et de leurs bordures en ajoutant des arguments supplémentaires :
```{r}
my_barplot <- function(var, col = "skyblue", border = NA) {
tri <- table(var)
barplot(tri, col = col, border = border)
}
```
Mais si on veut aussi permettre de personnaliser d'autres arguments de `barplot` comme `main`, `xlab`, `xlim`... il faudrait rajouter autant d'arguments supplémentaires à notre fonction, ce qui deviendrait vite ingérable. Une solution est de "capturer" tous les arguments supplémentaires avec `...` et de les passer directement à `barplot`, de cette manière :
```{r}
my_barplot <- function(var, ...) {
tri <- table(var)
tri <- sort(tri)
barplot(tri, ...)
}
```
Ce qui permet d'appeler notre fonction avec tous les arguments possibles de `barplot`, par exemple :
```{r}
my_barplot(
hdv2003$clso,
col = "yellowgreen",
main = "Croyez-vous en l'existence des classes sociales ?"
)
```
### Résultat d'une fonction {#sec-resfunc}
On l'a vu, l'objectif d'une fonction est en général de renvoyer un résultat. Lors de la définition d'une fonction, le résultat peut être retourné en utilisant la fonction `return()` :
```{r}
ajoute2 <- function(x) {
res <- x + 2
return(res)
}
```
En réalité, l'utilisation de `return()` n'est pas obligatoire : une fonction retourne automatiquement le résultat de la dernière instruction qu'elle exécute. On aurait donc pu écrire :
```{r}
ajoute2 <- function(x) {
res <- x + 2
res
}
```
Ou même, encore mieux et plus lisible :
```{r}
ajoute2 <- function(x) {
x + 2
}
```
::: {.callout-warning}
Dans la suite de ce document on utilisera, lorsque c'est possible, la syntaxe la plus "compacte" qui omet le `return()`.
:::
Un point important à noter : lorsque R rencontre une instruction `return()` dans une fonction, il interrompt immédiatement son exécution et "sort" de la fonction en renvoyant le résultat.
Ainsi, dans la fonction suivante :
```{r}
ajoute2 <- function(x) {
return(x + 2)
x * 5
}
```
L'instruction `x * 5` ne sera jamais exécutée car R "sort" de la fonction dès qu'il évalue le `return()` de la ligne précédente.
Conséquence de ce comportement, on ne peut pas utiliser plusieurs `return()` pour renvoyer plusieurs résultats depuis une seule fonction. Est-ce à dire qu'une fonction R ne pourrait renvoyer qu'une seule valeur ? Non, car si elle ne peut retourner qu'un seul objet, celui-ci peut être complexe et comporter plusieurs valeurs.
Par exemple, on a vu précédemment une fonction rudimentaire nommée `indicateurs()` qui affiche la moyenne et l'écart-type d'un vecteur numérique.
```{r}
indicateurs <- function(v) {
print(mean(v))
print(sd(v))
}
```
Plutôt que de se contenter de les afficher dans la console, on pourrait vouloir retourner ces deux valeurs pour pouvoir les réutiliser par la suite. Pour cela, une première solution pourrait être de renvoyer un vecteur comportant ces deux valeurs.
```{r}
indicateurs <- function(v) {
moyenne <- mean(v)
ecart_type <- sd(v)
c(moyenne, ecart_type)
}
```
```{r}
indicateurs(hdv2003$age)
```
Mais dans ce cas de figure il est recommandé de retourner plutôt une liste nommée^[Les listes seront abordées un peu plus en détail @sec-listes.], de cette manière :
```{r}
indicateurs <- function(v) {
moyenne <- mean(v)
ecart_type <- sd(v)
list(moyenne = moyenne, ecart_type = ecart_type)
}
```
```{r}
indicateurs(hdv2003$age)
```
On a du coup un affichage un peu plus lisible, et on peut accéder aux éléments du résultat via leur nom :
```{r}
res <- indicateurs(hdv2003$age)
res$moyenne
```
## Portée des variables
Un point délicat mais important quand on commence à créer ses propres fonctions concerne la *portée des variables*, c'est-à-dire la façon dont les objets créés dans une fonction et ceux existant en-dehors "cohabitent". C'est une question assez complexe, mais seules quatre grandes règles sont réellement utiles au départ.
### Une fonction peut accéder à un objet extérieur
Si on fait appel dans une fonction à un objet qui n'existe pas et n'a pas été passé comme argument, on obtient une erreur.
```{r include=FALSE, cache=FALSE}
if (exists("obj")) rm("obj")
```
```{r, error=TRUE, cache=FALSE}
f <- function() {
obj
}
f()
```
Si on crée cet objet dans notre fonction avant de l'utiliser, on supprime évidemment l'erreur.
```{r}
f <- function() {
obj <- 2
obj
}
f()
```
Mais on peut aussi accéder depuis une fonction à un objet qui existe dans notre environnement au moment où la fonction a été appelée.
```{r}
f <- function() {
obj
}
obj <- 3
f()
```
Dans cet exemple, au moment de l'exécution de `f()`, comme `obj` n'existe pas au sein de la fonction (il n'a pas été passé comme argument ni défini dans le corps de la fonction), R va chercher dans l'environnement global, celui depuis lequel la fonction a été appelée. Comme il trouve un objet `obj`, il utilise sa valeur au moment de l'appel de la fonction.
### Les arguments et les objets créés dans la fonction sont prioritaires
Que se passe-t-il si un objet avec le même nom existe à la fois dans la fonction et dans notre environnement global ? Dans ce cas *R privilégie l'objet créé dans la fonction*.
```{r}
f <- function() {
obj <- 10
obj
}
obj <- 3
f()
```
Cette règle s'applique également pour les arguments passés à la fonction.
```{r}
f <- function(obj) {
obj
}
obj <- 3
f(20)
```
### Un objet créé dans une fonction n'existe que dans cette fonction
Autre règle importante : un objet créé à l'intérieur d'une fonction n'est pas accessible à l'extérieur de celle-ci.
```{r, error = TRUE}
f <- function() {
nouvel_objet <- 15
nouvel_objet
}
f()
nouvel_objet
```
Ici, `nouvel_objet` existe tant qu'on est dans la fonction, mais il est détruit dès qu'on en sort et donc inaccessible dans notre environnement global.
::: {.callout-warning}
Les objets créés dans notre session et qui existent dans notre environnement (tel que visible dans l'onglet *Environment* de RStudio) sont appelés des **objets globaux** : ils existent et sont accessibles pour les fonctions appelées depuis cet environnement. Les objets créés lors de l'exécution d'une fonction sont à l'inverse des **objets locaux** : ils n'existent qu'à l'intérieur de la fonction et pour la durée de son exécution. Si deux objets du même nom coexistent, l'objet local est prioritaire par rapport à l'objet global.
:::
### On ne peut pas modifier un objet global dans une fonction
Une conséquence importante de la troisième règle est qu'il n'est pas possible de modifier un objet de notre environnement global depuis une fonction^[En réalité c'est possible avec l'opérateur `<<-`, mais c'est fortement déconseillé dans la très grande majorité des cas.] :
```{r}
f <- function() {
obj <- 10
message("Valeur dans la fonction : ", obj)
}
obj <- 3
f()
obj
```
Pour comprendre le résultat obtenu, on peut essayer de décomposer pas à pas :
1. Au moment du `obj <- 3`, R crée un objet global nommé `obj` avec la valeur 3.
2. Quand on exécute `f()` et qu'on rencontre l'instruction `obj <- 10`, R crée un nouvel objet nommé `obj`, local celui-ci, avec la valeur 10. À ce moment-là on a donc deux objets distincts portant le même nom, l'un global avec la valeur 3, l'autre local avec la valeur 10. Comme l'objet local est prioritaire, c'est lui qui est utilisé lors de l'affichage du message.
3. Lorsqu'on sort de `f()`, l'objet local contenant la valeur 10 est détruit. Il ne reste plus que l'objet global avec la valeur 3. C'est donc lui qui est affiché lors du dernier appel à `obj`.
Pour les mêmes raisons, dans l'exemple suivant, le recodage appliqué à la variable `taille` du tableau `df` passé en argument à la fonction `recode_taille()` n'est pas conservé en-dehors de la fonction. Ce recodage n'existe que dans un tableau `d` local à la fonction, et détruit dès qu'on en est sorti.
```{r}
df <- data.frame(taille = c(155, 182), poids = c(65, 71))
recode_taille <- function(d) {
d$taille <- d$taille / 100
}
recode_taille(df)
# Le recodage n'est pas conservé
df
```
Si on souhaite modifier un objet global, on doit le passer comme argument en entrée de notre fonction, et le renvoyer comme résultat en sortie. Pour que le recodage précédent soit bien répercuté dans notre tableau `df`, on doit faire :
```{r}
recode_taille <- function(d) {
d$taille <- d$taille / 100
d
}
df <- recode_taille(df)
# Le recodage est bien conservé
df
```
## Les fonctions comme objets
Quand on crée une fonction, on la "nomme" en la stockant dans un objet. Cet objet peut être utilisé comme n'importe quel autre objet dans R. On peut ainsi copier une fonction en l'attribuant à un nouvel objet :
```{r}
f <- function(x) {
x + 2
}
g <- f
g(10)
```
On a déjà vu à de nombreuses reprises que quand on fournit juste un nom d'objet à R, celui-ci affiche son contenu dans la console. C'est aussi le cas pour les fonctions : dans ce cas c'est le code source de la fonction qui est affiché.
```{r}
f
```
### Passer des fonctions comme argument
Certaines fonctions sont prévues pour s'appliquer elles-mêmes à des fonctions. Par exemple, `formals` et `body` permettent d'afficher respectivement les arguments et le corps d'une fonction passée en argument.
```{r}
formals(f)
```
```{r}
body(f)
```
Il est donc possible de passer une fonction comme argument d'une autre fonction, comme dans `body(f)`. On a déjà vu un exemple de ce type de fonctionnement avec la fonction `tapply` @sec-tapply. Celle-ci prend trois arguments : un vecteur de valeurs, un facteur, et une fonction. Elle applique ensuite la fonction aux valeurs pour chaque niveau du facteur.
Par exemple, si on a un data frame avec une liste de fruits et leur poids :
```{r}
df <- data.frame(
fruit = c("Pomme", "Pomme", "Citron", "Citron"),
poids = c(147, 189, 76, 91)
)
df
```
On peut utiliser `tapply` pour calculer le poids moyen par type de fruit.
```{r}
tapply(df$poids, df$fruit, mean)
```
Si on souhaite plutôt calculer le poids maximal, il suffit de passer à `tapply` la fonction `max` plutôt que la fonction `mean`.
```{r}
tapply(df$poids, df$fruit, max)
```
Cette manière de transmettre une fonction à une autre fonction peut être un peu déroutante de prime abord, mais c'est une mécanique qu'on va retrouver très souvent dans les chapitres suivants.
::: {.callout-warning}
Si `f` est une fonction, il est important de bien faire la différence entre `f` et `f()` :
- `f` est la fonction en elle-même
- `f()` est le résultat de la fonction quand on l'exécute sans lui passer d'argument
Quand on passe une fonction comme argument à une autre fonction, on utilise donc toujours la notation sans les parenthèses.
:::
### Fonctions anonymes {#sec-fonctions-anonymes}
Dans le cas où on souhaite calculer quelque chose pour lequel une fonction n'existe pas déjà, on peut créer une nouvelle fonction :
```{r}
poids_moyen_kg <- function(poids) {
mean(poids / 1000)
}
```
Et la passer en argument à `tapply()` :
```{r}
tapply(df$poids, df$fruit, poids_moyen_kg)
```
Si on ne souhaite pas réutiliser cette fonction par la suite, on peut aussi définir cette fonction directement comme argument de `tapply` :
```{r}
tapply(df$poids, df$fruit, function(poids) {
mean(poids/1000)
})
```
Dans ce cas on a créé ce qu'on appelle une *fonction anonyme*, qui n'a pas de nom (elle n'a pas été stockée dans un objet), et qui n'existe que le temps de l'appel à `tapply`.
## Ressources
L'ouvrage *R for Data Science* (en anglais), accessible en ligne, contient un chapitre complet d'[introduction sur les fonctions](http://r4ds.had.co.nz/functions.html).
L'ouvrage *Advanced R* (également en anglais) aborde de manière très approfondie [les fonctions](https://adv-r.hadley.nz/functions.html) ainsi que la [programmation fonctionnelle](https://adv-r.hadley.nz/fp.html).
Le manuel officiel *Introduction to R* (toujours en anglais) contient une partie sur [l'écriture de ses propres fonctions](https://cran.r-project.org/doc/manuals/R-intro.html#Writing-your-own-functions).
## Exercices
### Introduction et exemples
**Exercice 1.1**
Écrire une fonction nommée `perimetre` qui prend en entrée un argument nommé `r` et retourne le périmètre d'un cercle de rayon `r`, c'est-à-dire `2 * pi * r` (`pi` est un objet R qui contient la valeur de $\pi$).
Vérifier avec l'appel suivant :
```{r echo=FALSE, ref.label='fct11'}
```
```{r}
perimetre(4)
```
::: {.solution-exo}
```{r fct11, eval=FALSE}
perimetre <- function(r) {
resultat <- 2 * pi * r
return(resultat)
}
```
:::
**Exercice 1.2**
Écrire une fonction `etendue` qui prend en entrée un vecteur numérique et retourne la différence entre la valeur maximale et la valeur minimale de ce vecteur.
Vérifier avec l'appel suivant :
```{r echo=FALSE, ref.label='fct12'}
```
```{r}
etendue(c(18, 35, 21, 40))
```
::: {.solution-exo}
```{r fct12, eval=FALSE}
etendue <- function(v) {
vmax <- max(v)
vmin <- min(v)
return(vmax - vmin)
}
```
:::
**Exercice 1.3**
Écrire une fonction nommée `alea` qui accepte un argument `n`, génère un vecteur de `n` valeurs aléatoires entre 0 et 1 avec la fonction `runif(n)` et retourne ce vecteur comme résultat.
::: {.solution-exo}
```{r eval=FALSE}
alea <- function(n) {
v <- runif(n)
return(v)
}
```
:::
Modifier la fonction pour qu'elle accepte deux arguments supplémentaires `min` et `max` et qu'elle retourne un vecteur de `n` valeurs aléatoires comprises entre `min` et `max` avec la fonction `runif(n, min, max)`.
::: {.solution-exo}
```{r eval=FALSE}
alea <- function(n, min, max) {
v <- runif(n, min, max)
return(v)
}
```
:::
Modifier à nouveau la fonction pour qu'elle retourne un vecteur de `n` nombres *entiers* aléatoires compris entre `min` et `max` en appliquant la fonction `trunc()` au vecteur généré par `runif()`.
Vérifier le résultat avec :
```{r echo=FALSE, ref.label='fct13'}
```
```{r}
v <- alea(10000, 1, 6)
table(v)
```
::: {.solution-exo}
```{r fct13, eval=FALSE}
alea <- function(n, min, max) {
v <- runif(n, min, max + 1)
v <- trunc(v)
return(v)
}
```
:::
**Exercice 1.4**
Écrire une fonction nommée `meteo` qui prend un argument nommé `ville` avec le corps suivant :
```{r eval=FALSE}
out <- readLines(paste0("https://v2.wttr.in/", ville, "?A"))
cat(out, sep = "\n")
```