15. Les fonctions

R permet la création d’objets function, que l’on peut affecter à des variables pour réutilisation ultérieure.

La structuration du code en fonctions élémentaires permet un gain de productivité et de lisibilité. Chaque fois qu’une portion de code doit être exécuté plusieurs fois avec seulement un changement de paramètres, on écrira une fonction.

Avertissement

Il y a débat pour savoir si le terme « plusieurs » fait référence à 2 instructions et plus, ou s’il est plus efficace de copier/coller/modifier la ligne de code lorsque deux instructions semblables sont répétées. Il n’y a aucune ambigüité pour 3 instructions semblables.

R met en oeuvre le paradigme de programmation fonctionnelle.

La plupart des fonctions de R Core sont elle mêmes écrites en R. C’est le cas de mean() et var() par exemple. Elle sont de ce point de vue tout à fait semblables aux fonctions écrites par l’utilisateur.

On définit une fonction avec l’instruction suivante:

> name <- function(arg_1, arg_2, …) expr

expr est une expression R, généralement une expression groupée. Les arg_i sont les arguments de cette fonction qui doivent être passés en paramètre lors de l’appel.

La valeur de retour de la fonction est l’évaluation de expr.

L’appel de la fonction prend la forme name(par_1, par_2, ...).

15.1. Un exemple

Une implémentation de la suite de Fibonacci peut être écrite de façon récursive:

fib_r <- function(n,a=1,b=1){
    if (n == 1) a
    else fib(n-1,b,a+b)
}

> fib_r(32)
[1] 2178309

ou itérative:

fib_i <- function(n) {
    if (n == 1) 1
    else if (n == 2) 1
    else {
        f <- c(1,1)
        for (i in 3:n) f <- c(0,f[1]) + f[2]
        f[2]
    }
}

> fib_i(32)
[1] 2178309

15.2. Définir des opérateurs binaires

Pour simplifier l’écriture et augmenter la lisibilité, on peut affecter un opérateur à une fonction. Si on choisit le caractère !, on définira la fonction de la façon suivanteas:

> "%!%" <- function(x, y) expr

On utilisera l’opérateur de la façon suivante:

> x %!% y

ce qui sera strictement identique à:

> f(x,y)

A titre d’exemple, R core utilise l’opérateur %*% pour la multiplication de matrices. Tidyverse utilise le pipe %>% pour chainer les opérations.

15.3. Arguments nommés et par défaut

On a déjà utilisé les arguments nommés lors de l’appel à certaines fonctions. En particulier, na.rm=TRUE est utilisé avec les fonction statistiques (mean() par exemple) pour écarter les NA avant traitement. Les arguments nommés peuvent être fournis lors de l’appel dans n’importe quelle position.

Les arguments nommés sont placés après les arguments positionnels.

On considère la fonction fun1() définie par:

> fun1 <- function(data, data.frame, graph, limit) {
    [function body omitted]
  }

La fonction fun1() peut être appelée de différentes manières:

> ans <- fun1(d, df, TRUE, 20)
> ans <- fun1(d, df, graph=TRUE, limit=20)
> ans <- fun1(data=d, limit=20, graph=TRUE, data.frame=df)

Il est souvent pratique de fournir des valeurs par défaut pour certains arguments. C’est le cas pour la fonction mean() pour laquelle la valeur par défaut de l’argument na.rm est FALSE:

> mean(c(1:10,NA,11:20))
[1] NA
> mean(c(1:10,NA,11:20), na.rm=TRUE)
[1] 10.5

Dans la plupart des cas, les arguments possèdent des valeurs par défaut et il n’est alors pas utile de les passer explicitement. Si on déinit la fonction fun1() par:

> fun1 <- function(data, data.frame, graph=TRUE, limit=20) { … }

si on veut l’appeler avec les paramètres par défaut, on utilisera l’instruction:

> ans <- fun1(d, df)

On peut également forcer la valeur d’un paramètre par défaut:

> ans <- fun1(d, df, limit=10)

Les valeurs par défaut ne sont pas obligatoirement des constantes. Ce peut également être des expressions impliquant d’autres arguments de la fonction.

15.4. L’ellipsis ...

Pour une utilisation optimale de fonctions, il faut pouvoir:

  1. pouvoir passer un nombre arbitraire de paramètres

  2. passer des paramètres à une fonction appelée dans le corps de la définition de la fonction appelante

On utilise pour cela un argument spécial appelé ellipsis : ...

La fonction de création de vecteur est un exemple de fonction acceptant un nombre arbitraire de paramètres:

> c(1,2,3)
[1] 1 2 3
> c("a", "b")
[1] "a" "b"

Si on jette un oeil à la documentation de la fonction c(), son premier argument est l’ellipsis.

Pour illustrer le second cas, on considère la fonction addPercent():

addPercent <- function(x, mult = 1, ...) {
  round(x * (1+mult/100), ...)
}

Dans ce premier appel, on n’utilise pas d’arguments supplémentaires, l’ellipsis ne capture donc rien, et les arguments par défaut de round sont utilisés (digits=0):

> addPercent(n, 3.45)
[1] 10 21 31 41 52

Dans le deuxième appel, l’argument supplémentaire digits=2 est capturé par l’ellipsis ... et est passé à la fonction round():

> addPercent(n, 3.45, digits=2)
[1] 10.34 20.69 31.04 41.38 51.73

Les paramètres capturés par l’ellipsis sont stockés dans une list et peuvent être référencés indépendamment dans le corps de la fonction.

15.5. Variables locales et globales

L’opérateur d’affection traditionnel <- utilisé dans le corps de la fonction déclare des variables locales. Pour donner une portée globale à une variable, on utilise un autre opérateur d’affectation <<-:

f <- function(n){
  v1 <- n+1         # variable locale
  v2 <<- n+1        # variable globale
  n+1
}

> f(3)
[1] 4
> v1
Error: object 'v1' not found
> v2
[1] 4