Pourquoi écrire des fonctions ?
Exemple
Exemple de code : une data.frame contient 4 colonnes de données de températures en degrés C°. On souhaite convertir les degrés Celsius en Farheneit selon la formule \(F° = C° * 9 / 5 + 32\) .
df <- data.frame (
temp_a = rnorm (20 , 2 ),
temp_b = rnorm (20 , 2 ),
temp_c = rnorm (20 , 2 ),
temp_d = rnorm (20 , 2 )
)
df$ a <- (df$ a * 9 / 5 ) + 32
df$ b <- (df$ b * 9 / 5 ) + 32
df$ c <- (df$ c * 9 / 5 ) + 32
df$ d <- (df$ a * 9 / 5 ) + 32
Motivation à l’écriture d’une fonction
Eviter les copier/coller, et les erreurs associées, factoriser le code et réutiliser du code existant
Faciliter la maintenance et les évolutions du code en un seul endroit
Faciliter la lecture et la compréhension du code en donnant un nom évocatif
Syntaxe
faire_une_fonction <- function (w, x, y = 1 , z = NULL , ...) {
# commentaire pour les développeurs
if (! is.null (z)) {
return (z)
}
w + x + y # retourne le résultat de la dernière expression évaluée
}
fonction_sur_une_ligne <- function (x) x + 1
Les arguments peuvent prendre des valeurs par défaut ;
on peut écrire une fonction sur une ligne sans {}
si elle est suffisament simple ;
lorsque ce n’est pas trivial, il faut expliciter l’instruction de retour avec la fonction return()
.
Convertir en fonction
Ici, combien d’input(s) doit-on définir en entrée de la fonction ?
# conversion celsius/farheneit
df$ a <- (df$ a * 9 / 5 ) + 32
un seul input doit être défini, correspondant à la variable df$a
.
Nommage des fonctions : les fonctions traduisent des actions. Il faut donc utiliser un verbe pour traduire cette action dans le nom. On privilégiera le snake_case
.
Convertir en fonction
Quelle devrait être la nature de l’objet x ? Vecteur ou data.frame ?
convert_celsius_to_farheneit <- function (x) {
}
Convertir en fonction
Quelle devrait être la nature de l’objet x ? Vecteur ou data.frame ?
convert_celsius_to_farheneit <- function (x) {
x_farheneit <- (x * 9 / 5 ) + 32
return (x_farheneit)
}
Il est important de définir les paramètres comme étant les objets minimaux nécessaires à la fonction, donc ici un vecteur plutôt qu’une data.frame.
Convertir du code en fonction
Ctrl+Alt+X
sous Windows, Control+Option+X
sous Mac.
Principes de développement
Il faut essayer de suivre certains principes de design de code :
Limiter le nombre d’arguments à ses fonctions (4 ? …)
Utiliser des noms explicites, et des verbes
Définir des fonctions pures dans la philosophie de la programmation fonctionnelle .
Utiliser des objets : S3, S4, R6 …
Nous verrons comment tester, documenter et packager ses fonctions lors du cours n° 3
Définition d’une fonction
Les fonctions
Les fonctions sont des objets.
f <- function (x, y) {
# commentaire
x + y
}
Les fonctions
Les fonctions sont des objets
f <- function (x, y) {
# commentaire
x + y
}
body (f) # défini explicitement, code de la fonction
formals (f) # défini explicitement, ses arguments
environment (f) # basé sur où est définie la fonction
#> <environment: R_GlobalEnv>
3 types de fonctions
typeof (mean)
typeof (sum)
typeof (` [ ` )
3 types de fonctions
typeof (mean)
typeof (sum)
typeof (` [ ` )
#> [1] "closure"
#> [1] "builtin"
#> [1] "special"
La plupart des fonctions sont de type closure
Sauf des fonctions spéciales maintenues et écrites par R-core, builtin
et special
Scope d’une fonction
x <- 4
y <- 5
f00 <- function (x) {
return (x + 1 )
}
f00 (1 )
f00 (x)
f00 (y)
Scope d’une fonction
x <- 4
y <- 5
f00 <- function (x) {
return (x + 1 )
}
f00 (1 )
Scope d’une fonction
y <- 5
f01 <- function () {
return (y + 1 )
}
print ("premier appel" , f01 ())
y <- 10
print ("second appel" , f01 ())
Scope d’une fonction
y <- 5
f01 <- function () {
return (y + 1 )
}
f01 ()
y <- 10
f01 ()
La valeur de y est définie au moment de l’exécution, et non au moment de la définition de f01()
.
Scope d’une fonction
x <- 3
y <- 5
f02 <- function () {
x <- 2
f_in <- function () {
x + y
}
print (environment (f_in))
f_in ()
}
print (environment (f02))
f02 ()
x <- 5
f02 ()
Scope d’une fonction
x <- 3
y <- 5
f02 <- function () {
x <- 2
f_in <- function () {
x + y
}
print (environment (f_in))
f_in ()
}
print (environment (f02))
f02 ()
x <- 5
f02 ()
#> <environment: R_GlobalEnv>
#> <environment: 0x1117b6320>
#> [1] 7
#> <environment: 0x1130794d8>
#> [1] 7
Scope d’une fonction
Si un nom n’est pas défini dans l’environnement d’une fonction, alors on regarde “un niveau au-dessus”, dans l’environnement parent.
Un nom défini dans l’environnement de la fonction “masque” la valeur d’un objet de même nom défini dans un environnement parent.
R cherche la valeur quand la fonction est exécutée, non quand la fonction est définie.
R fait la différence entre les fonctions et les variables.
Ne pas nommer ses variables du même nom qu’une fonction
Faire des fonctions “pures” et définir les variables utiles dans les paramètres de la fonction
Arguments
f01 <- function (a, b) a + b** 2
f01 (1 , 2 )
f01 (b = 1 , 2 )
les paramètres non-nommés sont associés à l’argument positionnellement
il est possible de mélanger les paramètres nommés et non-nommés. Attention à l’ordre de ceux-ci !
Matching partiel
En R, les arguments sont identifiés partiellement !
f02 <- function (x, xyz, xylophone) x + xylophone** 2
f02 (1 , 2 , xylo = 3 )
f02 (1 , 2 , xy = 3 )
Matching partiel
En R, les arguments sont identifiés partiellement !
f02 <- function (x, xyz, xylophone) x + xylophone** 2
f02 (1 , 2 , xylo = 3 )
#> Error in f02(1, 2, xy = 3): argument 3 matches multiple formal arguments
Utilisez des arguments nommés si possible
Essayez de respecter l’ordre des arguments
Privilégiez le nom exact de l’argument
… (dot-dot-dot)
La notation ...
permet de propager des arguments à une autre fonction. Dans d’autres langages de programmation, on retrouve ce concept de varargs
soit arguments variables. Ceci permet à la fonction de prendre un nombre variable d’arguments.
f <- function (...) list (...)
f (a = 2 , b = "hello" )
f (x = c (1 , 2 , 3 ))
… (dot-dot-dot)
La notation ...
permet de propager des arguments à une autre fonction. Dans d’autres langages de programmation, on retrouve ce concept de varargs
soit arguments variables. Ceci permet à la fonction de prendre un nombre variable d’arguments.
f <- function (...) list (...)
f (a = 2 , b = "hello" )
#> $a
#> [1] 2
#>
#> $b
#> [1] "hello"
… (dot-dot-dot)
Utile pour passer des arguments supplémentaires, en particulier quand un des arguments de la fonction est lui même une fonction, par exemple pour lapply
Quelques points de vigilance
peut laisser passer des ‘typos’ sans lever d’erreurs ;
rend plus difficile la compréhension de ce que fait la fonction et nécessite une documentation précise.
sapply (list (c (1 : 10 ), c (1 : 10 , NA )), mean, na.rm = TRUE )
sapply (list (c (1 : 10 ), c (1 : 10 , NA )), mean, na_rm = TRUE )
Output d’une fonction
convert_celsius_to_farheneit <- function (x) {
x_farheneit <- (x * 9 / 5 ) + 32
return (x_farheneit)
}
Similaire à :
convert_celsius_to_farheneit <- function (x) {
x_farheneit <- (x * 9 / 5 ) + 32
x_farheneit
}
Une fonction retourne l’objet contenu dans l’appel explicite à return()
ou le résultat de la dernière expression évaluée de manière implicite.
Contrôle d’arguments
C’est une bonne pratique de vérifier des conditions importantes sur les arguments.
f04 <- function (x, y) {
x + y
}
Contrôle d’arguments
C’est une bonne pratique de vérifier des conditions importantes sur les arguments.
f04 <- function (x, y) {
if (! is.numeric (x)) stop ("x not numeric" )
if (! is.numeric (y)) stop ("y not numeric" )
x + y
}
f04 (1 , 2 )
f04 (1 , "a" )
Contrôle d’arguments
C’est une bonne pratique de vérifier des conditions importantes sur les arguments.
f04 <- function (x, y) {
if (! is.numeric (x)) stop ("x not numeric" )
if (! is.numeric (y)) stop ("y not numeric" )
x + y
}
f04 (1 , 2 )
#> Error in f04(1, "a"): y not numeric
Contrôle d’arguments
C’est une bonne pratique de vérifier des conditions importantes sur les arguments.
f04 <- function (x, y) {
stopifnot (is.numeric (x), is.numeric (y))
x + y
}
f04 (1 , 2 )
f04 (1 , "a" )
Contrôle d’arguments
C’est une bonne pratique de vérifier des conditions importantes sur les arguments.
f04 <- function (x, y) {
stopifnot (is.numeric (x), is.numeric (y))
x + y
}
f04 (1 , 2 )
#> Error in f04(1, "a"): is.numeric(y) is not TRUE
Erreurs, avertissements, messages
Trois types de signaux : errors
, warning
, message
. En R, il est possible d’attraper les erreurs ou les messages.
?tryCatch
?try
?withCallingHandlers
Opérateurs et fonctions spéciales
Quelques fonctions ont un comportement spécial : elles s’écrivent entre les arguments. Exemples, les opérateurs mathématiques :
On aurait pu écrire :
Liste des fonctions de ce type : :
, ::
, :::
, $
, @
, ^
, *
, /
, +
, -
, >
, >=
, <
, <=
, ==
, !=
, !
, &
, &&
, |
, ||
, ~
, <-
, ->
, <<-.
, etc.
Opérateurs et fonctions spéciales
Définir ses propres opérateurs :
` %+% ` <- function (lhs, rhs) {
paste0 (lhs, rhs)
}
"hell" %+% "o"
Autres fonctions spéciales :
(
, {
, [
, [[
, next
, repeat
, break
, if
, for
, while
, repeat
, function
…
Opérateurs et fonctions spéciales : pipe et composition de fonctions
Composition de fonctions :
square <- function (x) x^ 2
deviation <- function (x) x - mean (x)
x <- runif (100 )
# Population standard deviation
sqrt (mean (square (deviation (x))))
Avec des pipes, magrittr
ou R base (>4.1) :
x |> # R base (>4.1)
deviation () |>
square () |>
mean () |>
sqrt ()
x %>% # maggritr
deviation () %>%
square () %>%
mean () %>%
sqrt ()
Fonctions anonymes
Parfois, il n’est pas nécessaire de choisir d’associer un nom à une fonction, et on peut utiliser des fonctions anonymes.
x <- 1 : 3
sapply (x, function (x) x** 2 - 1 )
integrate (function (x) 1 / ((x + 1 ) * sqrt (x)), lower = 0 , upper = Inf )
#> 3.141593 with absolute error < 2.7e-05
liste <- list (
f = function (x) x** 2 ,
g = \(x) x - 1 ) # syntaxe possible depuis R > 4.1
liste$ f (10 )
Exemple : Date
date_cours <- as.Date ("2023-01-13" , format = "%Y-%m-%d" )
str (date_cours)
#> Date[1:1], format: "2023-01-13"
Les objets de type Date
sont de classe Date
, même si leur représentation interne est double
1 .
Attribut de classe
#> int [1:10] 1 2 3 4 5 6 7 8 9 10
class (x) <- "ma_premiere_classe"
str (x)
#> 'ma_premiere_classe' int [1:10] 1 2 3 4 5 6 7 8 9 10
#> [1] "ma_premiere_classe"
Un objet S3 est défini par son attribut class
. R est très permissif.
mod <- lm (log (mpg) ~ log (disp), data = mtcars)
class (mod)
class (mod) <- "Date"
print (mod)
#> Error in as.POSIXlt(.Internal(Date2POSIXlt(x, tz)), tz = tz): 'list' object cannot be coerced to type 'double'
S3
R n’a aucun moyen d’assurer que tous les objets d’une classe aient la même structure. Il faut aider l’utilisateur en définissant :
un constructeur
un validateur
un wrapper user-friendly autour du constructeur si l’objet a vocation à être exposé.
Voir le chapitre 13.3 d’Advanced R pour plus de détails et d’informations sur la philosophie des objets S3.
Fonctions et méthodes génériques
Une classe S3 permet le dispatch de fonctions génériques.
# définir une fonction générique :
nouvelle_fonction_generique <- function (x) {
UseMethod ("nouvelle_fonction_generique" )
}
Fonctions génériques courantes : print
, summary
, plot
…
Facilite l’apprentissage des APIs des librairies pour les utilisateurs
Le dispatch permet de gérer l’héritage de classe. R applique la bonne méthode au bon objet.
Exemple
new_integer <- function (x) {
stopifnot (is.integer (x))
structure (x, class = "new_integer" ) # altern. à `class<-`
}
print.new_integer <- function (x, ...) {
cat ("new_integer object \n " )
r <- range (x, ...)
cat ("min_value: " , r[[1L]], " - max_value: " , r[[2L]], " \n " )
cat (head (x), "......" , tail (x), " \n " )
invisible (x)
}
x <- new_integer (1 : 40L)
print (x)
#> new_integer object
#> min_value: 1 - max_value: 40
#> 1 2 3 4 5 6 ...... 35 36 37 38 39 40
print (unclass (x)) # le même vecteur sans attribut de classe
#> [1] 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] 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
Autres paradigmes objets en R
R n’est pas connu pour être un langage de programmation objet. Et pourtant, outre S3, il existe d’autres paradigmes : S4 , RC , R6 …
Paradigmes parfois différents des paradigmes courants en Java, Python, etc ;
S3 doit être le choix par défaut; sa simplicité et flexibilité répond à de nombreux besoins.