# 0 argument
<- function() cat("hi there")
say_hi # try it: say_hi()
# pass arguments to internal functions with `...`
<- function(x, y, ...){
my_boxplot boxplot(x, y, ...)
}# try it: my_boxplot(iris$Petal.Length, iris$Species, col="grey50")
15 Éléments de programmation
15.1 Fonctions
Les fonctions sont des unités de code qui remplissent une fonction donnée qui n’existe nulle part dans R1. Elles sont au coeur de R, et plus largement de tous les environnements de programmation. Paraxalement et même si elles peuvent effrayer au début, il n’y a pas grand chose à connaître sur les fonctions !
Définir une fonction plutôt que copier-coller son code à chaque fois que vous en avez besoin est non seulement bien pratique mais aussi plus sûr : si vous avez une modification à faire vous la faites une fois dans la définition de fonction, pas partout.
Idéalement une fonction fait une seule chose mais bien ! Faire des fonctions qui font aussi le café est un tropisme de débutant·e·s mais aue l’on perd vite. Oubliez donc
all_my_PhD_results()
et décomposez vos fonctions. Vos collaborateurs et vous-même dans six mois (c’est pareil), vous en sauront gré.Idéalement toujours, sauf pour les cas les plus triviaux, une fonction est documentée sur le modèle de la doc de R: c’est à dire son fonctionnement, les différents arguments, ce qu’elle retourne, des détails éventuels et des exemples. Commentez également l’intérieur de vos fonctions, ce sera utile pour tous y compris vous-même et tout de suite.
Pour les scripts les plus complexes, vous pouvez regrouper vos fonctions dans un script indépendant, et le
source
r en début de votre script d’analyse ou de votre document Quarto2.
Ne soyez pas timides, crééz vos fonctions à chaque fois que vous répétez le même code
Passons aux choses sérieuses.
Les fonctions sont définies avec la fonction fonction
suivant le patron :
nom_fonction <- function(arg1=default1, arg2=default2, ...) {
# instructions
return()
}
- Une fonction peut avoir zéro, un, plusieurs, ou même un nombre indéfini d’arguments.
Une fonction a des arguments nommés. Quand c’est possible avec une valeur par défaut. Ces valeurs peuvent être calculées “à la volée” sur d’autres arguments (voir
by
dans?seq
).Une fonction peut utiliser une fonction comme argument (
FUN
dansapply
par exemple)Une fonction peut retourner une fonction3
- Le code d’une fonction est accessible, la plupart du temps, en tapant le nom de la fonction sans parenthèse :
say_hi
function() cat("hi there")
Une fonction ne peut retourner qu’un seul objet. Si vous avez plusieurs, il vous faut en passer par une liste, si possible nommée :
return(list(res1=... res2=...))
Idéalement, une fonction peut parer à toutes les situations avec un message d’erreur informatif ou a minima une gestion de l’erreur. Je vous mets à l’aise, ce n’est presque jamais le cas :
plot("a")
n’est pas vraiment explicite et ne point pas le vrai problème.Quand vous commencez à avoir un joli paquet de fonctions, il est temps de penser à créer un package, pour vous-même ou pour le monde et dans un premier temps de lire la section consacrée plus loin.
15.2 Un mot sur les méthodes
Les fonctions peuvent avoir un comportement différent selon la classe de l’argument sur lequel elles opèrent. Un exemple trivial est summary
qui retourne des quantiles quand on lui passe un numeric
, et un descriptif plus sommaire quand on lui passe un character
:
summary(1:5)
Min. 1st Qu. Median Mean 3rd Qu. Max.
1 2 3 3 4 5
summary(letters[1:5])
Length Class Mode
5 character character
summary
est une méthode plus qu’une fonction, une façon de faire une chose (un résumé en l’occurence) sur une variable qui dépend de la classe de cette variable.
Voyons d’abord comment déclarer une méthode. Nous allons l’appeler shout
, pour crier en anglais. Quand shout
sera appelée sur un character
elle le passera en majuscules; sur un numeric
elle élèvera au carré.
<- function(x) {
shout UseMethod("shout", x)
}
<- function(x) {
shout.default stop("'shout' not defined on this class")
}
<- function(x) {
shout.character toupper(x)
}
<- function(x) {
shout.numeric ^2
x
}
# shout(iris) # try this on your machine
shout("let's be quiet")
[1] "LET'S BE QUIET"
shout(1:5)
[1] 1 4 9 16 25
C’est bien joli tout ça, mais n’aurait-on pas pu caler un bon vieux is.numeric
et tout déclarer dans une fonction. Oui, bravo, vous avez raison (j’ai créé un monstre). Mais pas vraiment (du calme jeune padawan) car les méthodes peuvent faire bien plus que cela, et notamment donner une saveur “orientée objet” à R.
Il existe plusieurs systèmes de déclaration de méthodes en R, et plus largement de programmation orientée objet, on peut citer S3
, S4
, R6
, etc. Le présent document ne s’attardera pas plus sur les méthodes mais les lignes ci-dessous démineront probablement quelques mystères de R.
Pour afficher toutes les méthodes, on utilisera la fonction methods
qui peut s’appeler soit sur le nom soit sur la classe :
methods("shout")
[1] shout.character shout.default shout.numeric
see '?methods' for accessing help and source code
methods(class="character")
[1] all.equal as.data.frame as.Date
[4] as.POSIXlt as.raster coerce
[7] coerce<- formula getDLLRegisteredRoutines
[10] Ops shout
see '?methods' for accessing help and source code
# try: methods("plot") # graphs, graphs, everywhere
# or even: methods("summary") # !!!
Comme on l’a vu précédemment, pour accéder au code d’une méthode on pourrait imaginer qu’il suffise, comme pour toute fonction de taper son nom à la console, sans parenthèses. Ici, le nom ne suffit pas, il faut également le suffixe de classe :
# not what I wanted but still, we know it's a method not a bare function shout
function(x) {
UseMethod("shout", x)
}
<bytecode: 0x7ffbf4ed5978>
shout.character
function(x) {
toupper(x)
}
shout.numeric
function(x) {
x^2
}
15.3 Un mot sur les packages
C’est un exercice très instructif, valorisant, valorisable et devenu quasiment facile de nos jours.
La référence absolue est R packages d’Hadley Wickham, qui a le bon goût d’être libre : https://r-pkgs.org/
Dura lex, sed lex, les tables de la loi sont plus dures mais ce sont les lois : https://cran.r-project.org/doc/manuals/r-release/R-exts.html
15.4 Control flow
Faisons comme Houellebecq et pompons Wikipédia qui écrit mieux que nous :
En programmation informatique, une structure de contrôle est une instruction particulière d’un langage de programmation impératif pouvant dévier le flot de contrôle du programme la contenant lorsqu’elle est exécutée. Si, au plus bas niveau, l’éventail se limite généralement aux branchements et aux appels de sous-programme, les langages structurés offrent des constructions plus élaborées comme les alternatives (if, if–else, switch…), les boucles (while, do–while, for…) ou encore les appels de fonction.
if
, else
, ifelse
, for
, while
, next
, break
En termes plus directs, le control flow dont disposent à peu près tous les langages de programmation, permet d’adapter le comportement d’un code en fonction des circonstances.
15.5 if
Imaginons par exemple que nous voulions écrire une fonction qui imprime à la console si un nombre est positif ou négatif. La structure if
permet de tester une condition logique et, généralement, d’agir en conséquence. ?Control
nous apprend que son patron général est le suivant :
if (condition) expr
condition
sera un test logique qui, s’il est vrai, exécutera la ou les commandes expr
. Si la commande est unique vous pouvez l’écrire en ligne. Si elle est multiple, on l’embrassera d’accolades :
if (condition) {
expr1
expr2
etc.
}
Notre fonction pourrait ressembler à :
<- function(x){
signe1 if (x > 0) {
cat(x, "est positif")
}if (x < 0) {
cat(x, "est négatif")
}
}
signe1(-1)
-1 est négatif
signe1(5)
5 est positif
Ici, nous omettons le classique return
car la fonction ne retourne rien, elle se contente d’émettre un message dans la console avec cat()
ce qui est très différent. Du reste, vous pouvez presque toujours omettre return
car une fonction retourne simplement la dernière valeur de son code. Ci-dessous, signe1()
ne retourne rien car toto
est NULL
(sauf si vous l’avez assigné avant naturellement).
<- signe1(1) toto
1 est positif
toto
NULL
15.6 else
Plutôt que d’empiler les if
, vous pouvez utiliser else
quand si ce n’est pas un cas, c’est forcément l’autre. Comme cela (à première vue) semble être le cas ici :
<- function(x){
signe2 if (x > 0) {
cat(x, "est positif")
else {
} cat(x, "est négatif")
}
}
signe2(-1)
-1 est négatif
signe2(5)
5 est positif
En réalité ici, on a une complication supplémentaire quand x=0
, que n’est pas prévu par signe1
et pire encore par signe2
(essayez donc signe1(0)
et signe2(0)
). Les if/else
peuvent être emboités et l’indentation de code se révèle particulièrement utile. (Code > Reindent lines
dans RStudio, ou <Ctrl>+A, <Ctrl>+I
, en remplaçant
<- function(x){
signe3 if (x==0) {
cat(x, "n'est ni négatif ni positif")
else {
} if (x > 0) {
cat(x, "est positif")
}if (x < 0) {
cat(x, "est négatif")
}
}
}
signe3(0)
0 n'est ni négatif ni positif
signe3(-1)
-1 est négatif
signe3(5)
5 est positif
Ce comportement de choix multiples se généralise au delà de deux avec switch
.
Vous pouvez tester plusieurs choses à la fois mais le résultat doit être un logical
de longueur 1, c’est à dire soit TRUE
soit FALSE
. Si par exemple vous voulez tester si un nombre est positif ET inférieur à 10 alors vous utiliserez un patron de ce genre pour cond
:
if ((x > 0) & (x < 10))
Finis les messages à la console, nous allons désormais retourner des nombres, en l’occurence -1
si x
est négatif, 1
sinon (zéro y compris):
<- function(x) {
signe4 if (x<0)
-1
else
1
}
signe4(-1)
[1] -1
signe4(0)
[1] 1
signe4(1)
[1] 1
Au passage, un exemple d’omission d’accolades lorsqu’une seule ligne est à exécuter. Cette structure aussi compacte avec un seul if
et un seul else
, et surtout une seule valeur retournée peut s’écrire de façon plus compacte avec ifelse
suivante le patron : ifelse(cond, expr_ifTRUE, expr_ifFALSE)
dans ce goût là : signe5 <- function(x) ifelse(x<0, -1, 1)
À ce moment de votre existence, vous vous dites “génial, je vais pouvoir balancer un vecteur à signe5
et aller à la plage”. Modérez votre enthousiasme :
signe4(-1:1)
Warning in if (x < 0) -1 else 1: the condition has length > 1 and only the first
element will be used
[1] -1
Cette commande aurait pu marcher, via un recyclage dans votre dos, mais ce n’est pas le cas, ce qui nous donne - heureux hasard - une transition rêvée vers for
. Notez bien que je vous montre for
mais que normalement vous ne devriez presque jamais en avoir besoin grâce à map
et ses variantes, dans le package purrr
.
15.7 for
Parcourir les valeurs d’un vecteur, d’une matrice, etc. et agir avec ces valeurs est une tâche très courante en programmation. for
permet de balayer un vecteur donné et d’assigner temporairement cette valeur à une autre variable, généralement appelée i
:
for (i in 1:5) {
print(i^2)
}
[1] 1
[1] 4
[1] 9
[1] 16
[1] 25
Le code ci-dessus devrait être transparent. Une précision toutefois : je suis obligé de forcer l’impression à la console avec print
(une variante moins subtile de cat
) sinon ce qu’il se passe dans les accolades de for
y resterait, sans conséquence visible.
Si vous voulez utiliser les résultats d’un calcul au sein d’une boucle for
, ce qui est le cas le plus fréquent, vous devez l’assigner à un objet compatible préalablement crée. De plus, on utilise généralement un vecteur avec toutes les valeurs à balayer par i
. Plutôt que d’utiliser cette chose : 1:length(x)
, on préferera le plus court et souvent plus générique seq_along(x)
<- 1:5
x <- vector("numeric", length=length(x))
res for (i in 1:length(x)){
<- x[i]^2
res[i]
} res
[1] 1 4 9 16 25
Au risque de me répéter, vous ne devriez pas avoir trop besoin de for
si vous maîtrisez map
, qui est plus compact, plus explicite et le plus souvent plus rapide :
::map_dbl(1:5, ~.x^2) purrr
[1] 1 4 9 16 25
15.8 while
et al.
Je ne m’attarde pas sur les autres structures de contrôles, bien moins utilisées, très bien décrites ailleurs et plutôt compréhensibles si vous avez survécu jusqu’ici : while
, next
, repeat
, break
, etc.
pour donner un ordre de grandeur, sur mes cinq derniers projets de consulting, tous les scripts finaux dépassent les 500 lignes mais sourcent des scripts qui font plus de 1000 lignes, dont presque la moitié de commentaires !↩︎