On a découvert les fonctions dans le concept Bases, avec des exemples comme celui-ci :
squareit <- function(x) {
x * x
}
squareit(3)
#> [1] 9
# shorter form
squareit_short <- function(x) x ^ 2
Si on regarde de plus près la définition de squareit, on peut identifier plusieurs éléments :
x.{ }.squareit.En R, une fonction est un objet de première classe, tout comme les nombres et les strings.
Ainsi, squareit <- function... est une affectation qui, syntaxiquement, ressemble exactement à x <- 42.
Contrairement à d'autres langages de script comme Python et Julia, R ne fait pas de distinction claire entre arguments positionnels et arguments nommés.
Un appel de fonction peut transmettre des valeurs de manière positionnelle ou par leur nom. Cette seconde possibilité est utile pour les fonctions complexes qui prennent beaucoup d'arguments, dont l'ordre est difficile à retenir.
f <- function(x, y) x / y
# call positionally
f(4, 2)
#> [1] 2
# call by name
f(y = 2, x = 4)
#> [1] 2
On peut spécifier des valeurs par défaut pour les arguments dans la définition de la fonction, mais elles doivent venir après tous les arguments qui n'en ont pas.
On peut ensuite choisir d'accepter la valeur par défaut ou de la remplacer.
g <- function(x, y = 2) x / y
# default y value
g(6)
#> [1] 3
# explicit y value
g(6, 3)
#> [1] 2
Pour accepter un nombre arbitraire d'arguments supplémentaires, utilise un ... (points de suspension) dans la définition.
Il est possible de convertir en vecteur les valeurs supplémentaires passées à l'appel de la fonction, mais lis la suite pour découvrir une autre façon d'utiliser ces « dot args » (appelés « varargs » dans plusieurs autres langages).
var_f <- function(x, y, ...) {
print(c(...))
}
var_f(2, 3, "opt1", "opt2")
#> [1] "opt1" "opt2"
Plus haut, on a dit que les arguments formels et le corps sont tous les deux des composants d'une fonction.
En fait, il existe un troisième composant : l'environnement dans lequel la fonction est définie.
On peut l'illustrer avec le cas des fonctions imbriquées :
outer_func <- function(x) {
inner_func <- function(y) {
x * y
}
inner_func(3)
}
outer_func(5)
#> [1] 15
L'appel de fonction transmet x = 5 à la fonction externe, et cette valeur est disponible dans le corps de cette fonction.
La fonction interne fait partie du corps de la fonction externe et a accès à la valeur de x.
Autrement dit, x = 5 se trouve dans l'environnement de la fonction interne.
Techniquement, c'est ce qu'on appelle une closure.
L'environnement est particulièrement important avec les « dot args », car toutes les valeurs fournies de cette façon peuvent être transmises aux appels de fonction dans le corps de la fonction. La fonction externe n'a pas besoin de savoir ni de se soucier de ce que signifient les « dot args ».
f_var <- function(x, ...) {
sum(x, ...)
}
x <- c(1, 2, NA, 6)
# for sum(), na.rm defaults to FALSE
f_var(x)
#> [1] NA
# pass through the na.rm value
f_var(x, na.rm = TRUE)
#> [1] 9
Cette technique est largement utilisée par les bibliothèques du Tidyverse, comme stringr.
Beaucoup de fonctions de stringr sont une surcouche conviviale autour de fonctions de bas niveau de stringi et de R de base.
Les arguments supplémentaires fournis aux fonctions str_*() sont simplement transmis à ces fonctions de plus bas niveau.
Une fonction externe peut définir une fonction interne (nommée ou anonyme, comme décrit plus bas) et l'utiliser comme valeur de retour.
La fonction renvoyée inclura l'environnement dans lequel elle a été définie.
times_y <- function(x) {
# anonymous function - see next section
\(y) x * y
}
f <- times_y(3)
class(f)
#> [1] "function"
f(4)
#> [1] 12
Quand on définit une fonction, on lie généralement l'objet fonction obtenu à une variable.
x <- 2
# function definition captures x from the environment
doubleit_short <- function(y) x * y
Cela permet d'utiliser facilement la fonction plus tard dans le script, mais cette liaison n'est pas indispensable. Une fonction courte, utilisée une seule fois, peut être utile dans le contexte immédiat. Sans liaison de nom, on parle de fonction anonyme.
L'usage des fonctions anonymes est si courant que (depuis R v4.1.0) il existe une syntaxe abrégée pour les définir : remplace le mot function par une barre oblique inversée \.
Un exemple en a été donné dans la section précédente, sur les valeurs de retour.
R permet d'affecter une valeur à des éléments individuels d'un vecteur. Si on passe un vecteur comme argument d'une fonction et qu'on le modifie dans le corps de la fonction avant de le renvoyer, on obtient un vecteur modifié.
Mais qu'est-il arrivé au vecteur d'origine ?
f <- function(vec) {
vec[1] <- 42
vec
}
vals <- c(1, 3, 4)
# f() returns a modified vector
f(vals)
#> [1] 42 3 4
# the original vector is unchanged.
vals
#> [1] 1 3 4
R est un langage conçu pour la science des données. Collecter ces données peut coûter cher en temps, en efforts, et potentiellement des sommes d'argent à donner le vertige : il est important de ne pas les corrompre !
La politique générale (à quelques exceptions près) est celle du copy on modify. Si un objet (comme un vecteur) est modifié d'une manière qui pourrait poser problème par la suite, R renvoie une copie modifiée et laisse l'original intact.
Copier de grandes structures de données peut être coûteux en calcul, mais c'est généralement le moindre mal quand l'alternative est la corruption des données.
Attention aux opérations qui entraînent une copie répétée des mêmes données.
Pour paraphraser Hadley Wickham (auteur de Tidyverse) :
Les boucles ne sont pas lentes par nature, mais elles rendent dangereusement facile l'inclusion d'opérations lentes à l'intérieur de la boucle.
La vectorisation ou les fonctions d'ordre supérieur peuvent t'aider à te protéger de ce genre de tueur de performances.
Ton entreprise travaille avec des données cartographiques pour construire des pages web. Après quelques discussions, la décision est prise de commencer à utiliser R pour effectuer certains calculs de manière dynamique.
Efforce-toi de travailler directement avec les points sous forme de vecteurs : inutile de les décomposer en coordonnées x et y séparées.
Implémente la fonction scale(point, s) qui prend un point avec un nombre arbitraire de dimensions et le met à l'échelle selon le s prédéfini.
# A 2D example
s <- c(0.5, 0.8)
point <- c(10, 5)
scale(point, s)
#> [1] 5 4
Implémente la fonction translate(point, ...) qui renvoie un nouveau point, dont chaque coordonnée est déplacée par les valeurs transmises dans les points de suspension.
Cette fonction doit pouvoir gérer des points de n'importe quelle dimension, le nombre d'arguments passés dans les points de suspension correspondant à cette dimension.
point2d <- c(2, 3)
# supply dx, dy translations
translate(point2d, 0.5, 0.6)
#> [1] 2.5 3.6
point3d <- c(2, 3, 4)
# supply dx, dy, dz translations
translate(point3d, 0.5, 0.6, 0.7)
#> [1] 2.5 3.6 4.7
Certains de tes collègues connaissent moins bien R, tu décides donc d'utiliser une fermeture de fonction pour créer des transformations réutilisables pour des paires de coordonnées {x, y}.
Implémente la fonction transform2d(dx, dy, s) qui renvoie une fonction utilisant une fermeture pour effectuer de manière répétable la translation et la mise à l'échelle d'un point en 2D.
L'argument de mise à l'échelle s doit être facultatif, avec une valeur par défaut de 1.
La fonction renvoyée doit prendre un point en 2D, puis :
dx et dy prédéfiniss
# scale all coordinates by the same amount (2) in this example
# your code needs to be able to handle a vector `s`
tf2d <- transform2d(0.5, 0.6, 2)
class(tf2d)
#> [1] "function"
tf2d(c(2, 3))
#> [1] 5.0 7.2
# Accept default for `s`
tf2d <- transform2d(0.5, 0.6)
tf2d(c(2, 3))
#> 2.5 3.6
L'ordre des opérations est important : déplacer puis mettre à l'échelle donne le bon résultat, l'inverse non. En langage technique, ces opérations ne commutent pas.
Les données cartographiques peuvent aussi contenir des hauteurs, utilisées pour ajouter des ombrages et des contours à l'écran.
Implémente la fonction transform3d(dx, dy, dz, s) qui renvoie une fonction.
L'argument de mise à l'échelle s doit être facultatif, avec une valeur par défaut de 1.
La fonction renvoyée doit prendre un point en 3D, le déplacer selon les valeurs prédéfinies, puis le mettre à l'échelle selon s.
# using default `s`
tf3d <- transform3d(0.5, 0.6, 0.7)
point <- c(2, 3, 4)
tf3d(point)
#> [1] 2.5 3.6 4.7
# using vector `s`
tf3d <- transform3d(0.5, 0.6, 0.7, c(0.5, 0.6, 0.7))
tf3d(point)
#> [1] 1.25 2.16 3.29
Inscris-toi sur Exercism pour apprendre et maîtriser R avec 21 concepts111 exercices, et un vrai mentorat humain, le tout gratuitement.