Définition de fonctions en R

Définition de fonction

En R, on définit une fonction de cette manière :

La fonction super_calcul a plusieurs paramètres : a, b et multiplication. On donne à ces paramètres des valeurs qu’on appelle arguments. Toutefois, dans le langage courant, et peut-être dans cette formation si je ne fais pas trop attention, on utilise parfois indistinctement les deux mots.

On appelle une fonction de cette manière :

Les arguments s’évaluent dans l’ordre qu’ils sont stipulés lors de la définition, on peut donc écrire de manière équivalente :

On remarque que dans la définition de fonction, on avait écrit multiplication = TRUE. Cela correspond à un argument par défaut. Si on omet de lui donner une valeur, sa valeur par défaut sera TRUE comme on peut le vérifier ci-dessous.

Si la valeur que l’on veut utilise est différente de l’argument par défaut, on est obligé de la saisir.

On peut, lors de l’appel, nommer explicitement certains arguments et pas d’autres. Si on procède ainsi, les arguments non-nommés sont attribués de gauche à droite, parmi ceux qui ne sont pas nommés. Ainsi, tous les appels ci-dessous sont équivalents.

Si un paramètre n’a pas de valeur par défaut, une valeur doit obligatoirement être assignée lors de l’appel de fonction. C’est pourquoi le code ci-dessous renvoie une erreur.

Note

La dernière remarque n’est pas complètement vraie. Du vieux code persiste dans R, renseignant des arguments par défaut dans le code même de la fonction, à l’aide de if (missing(argument)) {}. Ceci est toutefois, en général, considéré comme une mauvaise pratique.

On vient de constater de manière implicite un aspect de R : par défaut, la valeur retournée par une fonction est la dernière valeur évaluée lors de son exécution (qui dépend éventuellement de structures telles que if () else {}). Une manière explicite de renvoyer une valeur de retour est d’invoquer return(). Appeler return() met fin à l’exécution de la fonction. Tout ce qui vient après n’est jamais rappelé.

calcula() renvoie 2L, tandis que calculb() renvoie 3L. On aurait pu également constater que, dans calcula, si on remplace 3L par stop(), le stop n’empêche pas la fonction de retourner convenablement. En effet, il arrive après le return.

Dots

Lorsque le nombre de paramètres d’une fonction peut être variable, on peut utiliser les ... qu’on appelle “dots”. list(...) permet de retrouver ces arguments sous forme de liste dans le corps de la fonction.

Dans le code précédent, on a pu définir une fonction de somme sans figer à l’avance le nombre de termes de la somme.

Note

On a utilisé dans super_somme une fonction particulière, Reduce, qui est typique des langages fonctionnels. L’idée est ici que l’on calcule 2 + 3 + 7 + 1. On verra plus en détail ce type d’opérations dans la partie dédiée au paradigme fonctionnel.

Dans le cas présent, sum existe déjà et peut déjà s’appliquer à un vecteur. On n’a écrit super_somme qu’à fin d’illustration.

Appeler une fonction dots sur une liste

Imaginons que l’on veuille appeler une fonction ... sur une liste. On souhaite que chaque élément de la liste devienne un argument. On peut par exemple faire, sur notre fonction super_somme.

Note

La fonction do.call est en réalité plus générale que cela. Elle permet également d’utiliser des arguments nommés, comme on peut le voir dans l’exemple suivant :

Fonctions récursives

Les fonctions en R peuvent tout-à-fait être récursives. Par exemple, même si factorial() existe déjà dans R, on pourrait très bien la redéfinir via :

R permet également une petite astuce pour éviter d’avoir à utiliser le nom d’une fonction dans son propre corps (cela permet de pouvoir renommer la fonction à un seul endroit). Le code précédent est équivalent à :

Remarque optionnelle

Depuis R 4.4.0 sortie fin avril 2024 (pas avant !), on peut utiliser la fonction Tailcall pour faire des récursivités terminales ; qui sont moins pratiques à lire mais plus efficaces. L’interface n’est pas encore figée mais cela ressemble aujourd’hui à :

Exercices

Question 1

Ecrire une fonction super_produit <- function(...) qui calcule le produit de tous les arguments sous les dots, et l’utiliser pour écrire une nouvelle version de la fonction fact.

super_produit <- function(...) {
  dots <- list(...)
  Reduce(`*`, dots)
}
fact <- function(n) {
  do.call(super_produit, as.list(1L:n))
}
fact(10)
[1] 3628800

Question 2

Écrire une fonction dots usine_a_gaz qui renvoie :

  • NULL s’il n’y a aucun argument.
  • Le nombre d’arguments s’il y en a deux ou plus.
  • "bonjour" si on lui soumet en unique argument un vecteur de type character (is.character permet de faire le test).
  • La somme du vecteur si on lui soumet en unique argument un vecteur numérique au sens de is.numeric.
  • "échec" dans tous les autres cas.

On pourra éventuellement s’aider des petites astuces, dont ...length(), lisibles dans la page d’aide des .... Celle-ci est accessible en tapant help(dots).

usine_a_gaz <- function(...) {
  longueur <- ...length()
  if (longueur == 0) NULL
  else if (longueur >= 2L) longueur
  else if (is.character(..1)) "bonjour"
  else if (is.numeric(..1)) sum(..1)
  else "échec"
}
usine_a_gaz()
NULL
usine_a_gaz(1,3)
[1] 2
usine_a_gaz(c("a","b"))
[1] "bonjour"
usine_a_gaz(c(1, 2))
[1] 3
usine_a_gaz(c(1L, 2L))
[1] 3
usine_a_gaz(list())
[1] "échec"

On peut également utiliser des return(), auquel cas les else deviennent inutiles puisque return fait directement retourner la fonction.

usine_a_gaz <- function(...) {
  longueur <- ...length()
  if (longueur == 0) return(NULL)
  if (longueur >= 2L) return(longueur)
  if (is.character(..1)) return("bonjour")
  if (is.numeric(..1)) return(sum(..1))
  "échec"
}
usine_a_gaz()
NULL
usine_a_gaz(1,3)
[1] 2
usine_a_gaz(c("a","b"))
[1] "bonjour"
usine_a_gaz(c(1, 2))
[1] 3
usine_a_gaz(c(1L, 2L))
[1] 3
usine_a_gaz(list())
[1] "échec"

Question 3

Réécrire la fonction super_somme en utilisant une syntaxe récursive. On pourra, par exemple, s’aider de la fonction head, qui permet de retourner une sous-liste contenant les n premiers éléments d’une liste.

super_somme <- function(...) {
  dots <- list(...)
  longueur <- length(dots)
  if (longueur == 0L) 0
  else dots[[longueur]] + do.call(
    # Rajouter quelque chose dans le do.call qui rappelle super_somme sur les longueur - 1L premiers éléments 
  )
}
super_somme(2, 3, 7, 1)
super_somme <- function(...) {
  dots <- list(...)
  longueur <- length(dots)
  if (longueur == 0L) 0
  else dots[[longueur]] + do.call(super_somme, head(dots, longueur - 1L))
}
super_somme(2, 3, 7, 1)
[1] 13
Remarques
  • La récursivité est ici très bourrine ! On le fait juste pour l’exercice.
  • Quelques petites astuces permettent, si besoin, d’éviter de convertir tous les arguments d’une fonction dots en liste. On dispose par exemple de ...length() en tant que substitut pour length(list(...)).
  • On peut éventuellement utiliser Recall en tant que substitut pour super_somme dans le do.call.