3 Fonction

Jusqu’ici, nous avons utilisé des fonctions incluses dans les packages, rédigées par d’autres personnes. Dans ce chapitre, nous allons voir comment créer ses propres fonctions.

3.1 Définition

La définition d’une nouvelle fonction suit la syntaxe suivante :

name <- function(arguments) expression

avec name le nom que l’on décide de donner à la fonction, qui doit respecter les règles de nommage abordés en Section 1.2.2.2, arguments les arguments de la fonction, et expression le corps de la fonction. Comme on peut le remarquer, on utilise le symbole d’assignation : les fonctions sont des objets. L’appel de la fonction aura la syntaxe suivante :

name()

Il suffit donc de rajouter des parenthèses au nom de la fonction pour l’appeler. En effet, name désigne l’objet R~qui contient la fonction qui est appelée à l’aide de l’expression name().

Par exemple, si on souhaite définir la fonction qui calcule le carré d’un nombre, voici ce que l’on peut écrire :

carre <- function(x) x^2
# Le carré de 2
carre(2)
## [1] 4
# Le carré de -3
carre(-3)
## [1] 9

3.2 La structure d’une fonction

Les fonctions en R, excepté les fonctions primitives du package {base}, sont composées de trois parties :

  • une liste d’arguments ;
  • un corps, contenant du code exécuté lors de l’appel à la fonction ;
  • un environnement, qui définit l’endroit où sont stockées les variables.

On peut accéder à ces trois parties (et les modifier) avec les fonctions formals() pour les arguments, body() pour le corps et environment() pour l’environnement.

3.2.1 Le corps d’une fonction

Dans le cas le plus simple, le corps d’une fonction est constitué d’une seule instruction. Si on désire en écrire plusieurs, il est nécessaire de les entourner par des accolades, pour réaliser un regroupement. Le résultat est la valeur de la dernière commande contenue dans le corps de la fonction.

f <- function(x) {
  x^2
  y <- x
  y
}
f(2)
## [1] 2

Si on souhaite retourner une valeur autre part qu’à la dernière ligne, il faut utiliser la fonction return() (utile lorsque l’on emploie des conditions, comme nous le verrons par la suite, ou pour prévenir d’une erreur).

f <- function(x) {
  return(x^2)
  # Un commentaire de dernière ligne
}
f(2)
## [1] 4
Certains conseillent de ne pas utiliser la fonction return() en dernière ligne, dans la mesure où cela est d’une part inutile, et perturbe la lecture du code d’autre part.

Il est possible de retourner une liste, pouvant contenir autant d’objet que l’on souhaite.

# Calculer la moyenne et l'écart-type pour un vecteur
stat_des <- function(x) {
  list(moyenne = mean(x), ecart_type = sd(x))
}
x <- runif(10)
stat_des(x)
## $moyenne
## [1] 0.2964024
## 
## $ecart_type
## [1] 0.3340041

Il est également possible de ne pas afficher dans la console le résultat de l’appel à une fonction à l’aide de la fonction invisible().

stat_des_2 <- function(x) {
  invisible(list(moyenne = mean(x), ecart_type = sd(x)))
}
x <- runif(10)
stat_des_2(x)
str(stat_des_2(x))
## List of 2
##  $ moyenne   : num 0.612
##  $ ecart_type: num 0.321
stat_des_2(x)$moyenne
## [1] 0.6124115

On peut afficher malgré tout le résultat d’une fonction retournant un résultat invisible en ayant recours aux parenthèses.

(stat_des_2(x))
## $moyenne
## [1] 0.6124115
## 
## $ecart_type
## [1] 0.3212355

Lorsque la dernière instruction est une assignation, nous sommes dans le cas d’un résultat invisible.

f <- function(x){
  res <- x^2
}
f(2)
(f(2))
## [1] 4
x <- f(2)
x
## [1] 4

3.2.2 Les arguments d’une fonction

Dans l’exemple de la fonction carre() que nous avons crée, nous avons renseigné un seul argument, appelé x. Si la fonction que l’on souhaite créer nécessite plusieurs arguments, il faut les séparer par une virgule.

Considérons par exemple le problème suivant. Nous disposons d’une fonction de production \(Y(L,K, M)\), qui dépend du nombre de travailleurs \(L\) et de la quantité de capital \(K\), et du matériel \(M\), telle que \(Y(L,K, M) = L^{0.3}K^{0.5}M^{2}\). Cette fonction pourra s’écrire, en R de la manière suivante :

production <- function(l, k, m) l^(0.3) * k^(0.5) * m^(0.2)

3.2.2.1 Appel sans noms

En reprenant l’exemple précédent, si on nous donne \(L = 60\) et \(K = 42\) et \(M = 40\), on peut en déduire la production :

production(60, 42, 40)
## [1] 46.28945

On peut noter que le nom des arguments n’a pas été mentionné ici. Lors de l’appel de la fonction, R cherche d’abord s’il y a des arguments nommés afin de leur associer des valeurs. S’il n’y a pas de nom, il se basera sur la position donnée aux arguments.

production(k = 42, m = 40, l = 60)
## [1] 46.28945
production(k = 42, 60, 40)
## [1] 46.28945

3.2.2.2 Arguments effectifs

On peut, lors de la définition de la fonction, choisir de donner une valeur par défaut aux arguments. On parle d’argument formel pour désigner les arguments de la fonction (les variables utilisées dans le corps de la fonction) et d’argument effectif pour désigner la valeur que l’on souhaite donner à l’argument formel. Pour définir la valeur à donner à un argument formel, on utilise le symbol d’égalité. Lors de l’appel de la fonction, si l’utilisateur•rice ne définit pas explicitement une valeur, celle par défaut sera affectée.

# On propose de définir la valeur du capital à 42 par défaut
production_2 <- function(l, m, k = 42) l^(0.3) * k^(0.5) * m^(0.2)
production_2(l = 42, m = 40)
## [1] 41.59216
production_2(l = 42, m = 40, k = 2)
## [1] 9.076152

Dans l’exemple précédent, l’argument auquel nous avons donné une valeur est placé en dernier. Ce n’est pas obligatoire, mais plus pratique, si le but recherché est de ne pas avoir à saisir l’argument effectif lors de l’appel de la fonction. De plus, si l’utiliateur ne nomme pas les arguments lors de l’appel, des problèmes liés à l’ordre peuvent apparaître…

production_3 <- function(l, k = 42, m) l^(0.3) * k^(0.5) * m^(0.2)
production_3(l = 42, m = 40)
## [1] 41.59216
production_3(42, 40)
## Error in production_3(42, 40): argument "m" is missing, with no default

3.2.2.3 Appel avec des noms partiels

Par ailleurs, il est possible de ne pas saisir le nom complet des arguments lors de l’appel d’une fonction. En effet, on peut utiliser une abréviation du nom de l’argument. S’il existe une ambiguïté, R retourne un message d’erreur.

f <- function(premier, second, troisieme) premier + second + troisieme
f(p = 1, s = 2, t = 3)
## [1] 6
# Problème d'ambiguïté
f <- function(texte, nombre, nom) print(nom)
f("hello", 2, no = 3)
## Error in f("hello", 2, no = 3): argument 3 matches multiple formal arguments
Si parmi les arguments de la fonction, figure l’argument ... (plus de détails sont donnés dans la Section @ref(fonctions_structure_parametres_special), il n’est pas possible d’utiliser les abréviations.

3.2.2.4 Fonctions sans arguments

On est parfois amené à créer des fonctions qui ne prennent pas d’argument. Il suffit alors de laisser la liste d’arguments formels vide.

f <- function() sample(letters, size = 10, replace = TRUE)
f()
##  [1] "t" "w" "d" "p" "d" "y" "x" "x" "g" "j"

3.2.2.5 L’argument spécial ...

l’argument ... que l’on peut voir dans certaines fonctions (essayez d’évaluer sum par exemple) sert à indiquer que la fonction peut admettre d’autres arguments que ceux qui ont été définis. Cela sert à, dans la plupart des cas, à passer un argument à une autre fonction contenue dans le corps de la fonction que l’on appelle.

f <- function(...) names(list(...))
f(premier = 1, second = 2)
## [1] "premier" "second"

Attention toutefois, l’utilisation de ... peut induire des soucis. En effet, un argument mal écrit sera passé à ... et il n’y aura pas d’erreur de retournée. Par ailleurs, tous les arguments placés après ... doivent être complètement nommés, pas abrégés.

sum(3, NA, 4, na.rm = TRUE)
## [1] 7
sum(3, NA, 4, an.rm = TRUE) # Mauvaise écriture
## [1] NA
sum(3, NA, 4, na = TRUE)  # Abréviation
## [1] NA

3.2.3 Portée des fonctions

Lorsque une fonction est appelée, le corps de cette fonction est interprété. Les variables ayant été définies dans le corps de la fonction ne vivent qu’à l’intérieur de celle-ci à moins d’avoir spécifié le contraire. On parle alors de portée des variables. Ainsi, une variable ayant une portée locale — c’est-à-dire vivant uniquement à l’intérieur du corps de la fonction — peut avoir le même nom qu’une variable globale — c’est à dire définie dans l’espace de travail de la session —, sans pour autant désigner le même objet, ou écraser cet objet.

# Définition d'une variable globale
valeur <- 1

# Définition d'une variable locale à la fonction f
f <- function(x){
  valeur <- 2
  nouvelle_valeur <- 3
  print(paste0("valeur vaut : ",valeur))
  print(paste0("nouvelle_valeur vaut : ",valeur))
  x + valeur
}

f(3)
## [1] "valeur vaut : 2"
## [1] "nouvelle_valeur vaut : 2"
## [1] 5
# valeur n'a pas été modifiée
valeur
## [1] 1
# nouvelle_valeur n'existe pas en dehors de f()
nouvelle_valeur
## Error in eval(expr, envir, enclos): object 'nouvelle_valeur' not found

Sans trop rentrer trop dans les détails, il semble important de connaître quelques principes à propos de la portée des variables. Les variables sont définies dans des environnements, qui sont embriqués les uns dans les autres. Si une variable n’est pas définie dans le corps d’une fonction, R ira chercher dans un environnement parent.

valeur <- 1
f <- function(x){
  x + valeur
}
f(2)
## [1] 3

Si on définit une fonction à l’intérieur d’une autre fonction, et qu’on appelle une variable non définie dans le corps de cette fonction, R ira chercher dans l’environnement directement supérieur. S’il ne trouve pas, il ira chercher dans l’environnement encore supérieur, et ainsi de suite.

# La variable valeur n'est pas définie dans g(). R va alors chercher dans f().
valeur <- 1
f <- function(){
  valeur <- 2
  g <- function(x){
    x + valeur
  }
  g(2)
}
f()
## [1] 4
# La variable valeur n'est définie ni dans g() ni dans f()
# mais dans l'environnement supérieur (global, ici)
valeur <- 1
f <- function(){
  g <- function(x){
    x + valeur
  }
  g(2)
}
f()
## [1] 3

Si on définit une variable dans le corps d’une fonction et que l’on souhaite qu’elle soit accessible dans l’environnement global, on peut utiliser le symbole <<-, ou bien la fonction assign (pratique peu recommandable).

rm(x)
f <- function(x){
  x <<- x + 1
}
f(1)
x
## [1] 2
# En utilisant assign
rm(x)
f <- function(x){
  # envir = .GlobalEnv signifique que l'on veut définir dans l'environnement global
  assign(x = "x", value = x + 1, envir = .GlobalEnv)
}
f(4)
x
## [1] 5

3.3 Documentation

Lorsque l’on créé une fonction, il est important de bien la documenter afin de rendre son utilisation facilitée (par les autres mais également par soi-même).

Il existe un package ({roxygen2}) qui permet de réaliser des fichiers de documentation des fonctions que l’on souhaite exporter au sein d’un package. Ces notes de cours ne visent pas à expliquer comment créer un package. Toutefois, il n’est pas inutile d’emprunter une technique utilisée par de nombreuses personnes qui créent des packages pour améliorer la documentation de nos fonctions dans nos codes.

L’idée est d’ajouter un commentaire roxygen en préfixe de la définition de la fonction. Ce commentaire s’écrit à l’aide d’un croisillon suivi d’un guillemet simple : #'. Reprenons la fonction de production, et ajoutons un commentaire roxygen précisant :

  • ce que fait la fonction
  • quels sont ses arguments (@param)
  • ce qu’elle retourne (@return)
  • deux exemples d’utilisation
#' Calcul le niveau de production
#' 
#' @param l montant de force de travail
#' @param k montant de capital
#' @param m montant de matériel
#' @return Le niveau de production d'une Cobb-Doublas, fonction de \code{l}, \code{k} et \code{m}
#' @examples
#' production(k = 42, l = 60, m = 40)
#' production(40, 20, 60)
production <- function(l, k, m) {
  l^(0.3) * k^(0.5) * m^(0.2)
}

Plus de détails sont disponibles au chapitre 10 du livre “R Packages” d’Hadley Wickham et Jennifer Bryan.

3.4 Exercices

Exercice 1. Création de fonctions

  1. Créer une fonction nommée somme_n_entiers qui retourne la somme des \(n\) premiers entiers. Son seul argument sera n ; 2.Utiliser la fonction somme_n_entiers() pour calculer la somme des 100 premiers entiers ;

  2. Terminer la fonction par l’assignation du résultat dans un objet nommé res, puis évaluer l’expression suivante : somme_n_entiers(100). Que peut-on constater ?

  3. Charger les données diamonds du package {ggplot2} dans la session R à l’aide de l’expression suivante : data(diamonds, package = "ggplot2")

    Créer une fonction que l’on appellera prix_diamant_coupe(), qui, quand on lui fournit la valeur de la coupe du diamant sous forme de caractères (Fair, Good, Very Good, Premium, ou Ideal), filtre le tableau de données diamonds pour ne conserver que les observations pour lesquelles la coupe du diamant correspond à celle indiquée en argument, et retourne le prix moyen des observations de la base ainsi filtrée ;

  4. Reprendre le code de la fonction précédente, et le modifier pour retourner à présent une liste de deux éléments : (i) la moyenne des prix et (ii) l’écart-type ;

  5. Créer la fonction resume_diamant_coupe_couleur(), qui pour une coupe et une couleur de diamant données, retourne une liste de deux éléments : (i) la moyenne des prix et (ii) l’écart-type pour les diamants possédant cette coupe et cette couleur (la couleur du diamant est une lettre allant de J pour les pires, à D pour les meilleurs). Tester la fonction pour la coupe Fair et la couleur D ;

  6. Reprendre la fonction précédente, et lui attribuer la valeur D (en chaîne de caractères) comme argument effectif pour la couleur. Tester alors l’appel à la fonction en précisant :

  • la coupe Fair et la couleur D,
  • la coupe Fair, mais pas d’argument pour la couleur,
  • la coupe Fair et la couleur E,
  • la coupe non précisée mais la couleur E ;

Exercice 2. Création de fonctions

Supposons que les adresses e-mails des étudiant•e•s d’Aix-Marseille Université sont constituées de la manière suivante : le prénom et le nom de famille séparés par un point, le symbole arobase et le enfin le nom de domaine. Supposons de plus que les étudiant•e•s ont un seul prénom, et aucune particule au nom de famille. La syntaxe des adresses e-mail est donc comme suit : nom.prenom@etu.univ-amu.fr.

emails <- c("marie.petit@etu.univ-amu.fr", "jean.dupont@etu.univ-amu.fr", "isabelle.martinez@etu.univ-amu.fr", "pierre.moreau@etu.univ-amu.fr")

Créer une fonction, qui à partir d’une seule adresse e-mail d’un•e étudiant•e, retourne un tibble contenant trois variables : le prénom, le nom et l’adresse e-mail de cet•te étudiant•e.