Fonctions d’ordre supérieur (arguments)

Fonctions d’ordre supérieur

En informatique, la notion de fonction d’ordre supérieur désigne une fonction avec au moins l’une des propriétés suivantes :

  • Elle a pour argument une ou des fonctions.
  • Elles retournent a minima une fonction.

Les fonctions d’ordre supérieur sont un élément central permettant l’expressivité des langages fonctionnels.

Dans cette première partie sur les fonctions d’ordre supérieur, nous abordons la thématique des fonctions avec ayant des arguments fonctionnels.

Manipuler des fonctions avec des fonctions

On a déjà vu dans le point précédent deux fonctions qui s’appliquent sur des fonctions. do.call nous a permis d’appliquer une fonction de type function(...) sur une liste, tandis que Reduce nous a permis d’écrire 2 + 3 + 7 + 1 de manière un peu plus jolie qu’à la main.

On a vu dans l’introduction, également, que R est un langage principalement fonctionnel. R offre un certain nombre de fonctions permettant d’appliquer des fonctions de différentes manières. Nous verrons ici lapply, vapply, Map, replicate, apply, do.call, Reduce.

Remarque optionnelle

Un package du tidyverse, purrr, donne accès à des fonctions similaires à toutes celles décrites dans cette partie. Ce lien donne des équivalents purrristes des fonctions d’ordre supérieur abordées ici.

L’usage est selon les gouts et les couleurs. Utiliser les fonctions de base est suffisant et permet de se débarasser d’une dépendance, mais purrr est plus cohérent avec le reste de l’écosystème tidyverse. Par exemple, il permet de passer facilement au package de parallélisation furrr.

lapply : appliquer une fonction à chaque élément d’une liste ou d’un vecteur

La fonction function(x) c(x, x, x) demande à répéter trois fois un élément. Cette fonction est appliquée terme à terme à chaque élement de la liste.

  • c("a", "b") donne c("a", "b", "a", "b", "a", "b")
  • c("c", "d", "e") donne c("c", "d", "e", "c", "d", "e", "c", "d", "e")
  • c("f", "g") donne c("f", "g", "f", "g", "f", "g")

Le calcul ci-dessus montre qu’on peut utiliser lapply à l’intérieur d’une autre fonction.

On s’est d’ailleurs permis ici de remplacer function(x) c(x, x, x) par une fonction nommée qui s’appelle repeter. Ce petit changement illustre un aspect implicite des deux précédentes syntaxes utilisées : on utilisait function(x) c(x, x, x) en lieu et place d’un nom de fonction à l’intérieur du lapply ! On appelle de telles fonctions, dépourvues de noms, des fonctions anonymes. Si une fonction n’a pour vocation que d’être utilisée une seule fois, cela peut rendre la syntaxe plus claire et plus élégante qu’une fonction nommée.

Dans ce dernier calcul, on remarque que l’on peut appliquer lapply sur un vecteur.

Toutefois, dans les 4 calculs, le résultat de lapply est une liste. lapply retourne une liste même quand on lui soumet un vecteur.

Note

Dans les versions de R récentes, on peut utiliser la notation compacte \(x) c(x, x, x) à la place de function(x) c(x, x, x) pour désigner une fonction.

À l’intérieur de l’écosystème tidyverse, on peut également utiliser la notation ~c(.x, .x, .x).

vapply : une sorte de lapply mais avec un retour sous forme d’un vecteur ou d’une matrice.

On l’a vu dans les exemples précédents, le retour de lapply est une liste. Mais une liste n’est pas l’objet le plus pratique à manipuler dans un langage vectoriel ! la fonction vapply permet de retourner les résultats sous la forme d’un vecteur ou d’une matrice (lorsque cela a une pertinence). La syntaxe est un peu particulière mais on s’y habitue vite !

vapply applique la fonction anonyme \(x) x[1L] + 1L sur chaque élément de la liste list(c(3L, 3L, 10L), c(7L, -2L, 11L, 22L), -1L) et renvoie un vecteur de type similaire à 0L (c’est-à-dire un "integer").

Note

On rappelle que dans R, une variable comme -1L n’est rien d’autre qu’un vecteur de taille 1L. C’est pour cela qu’on a pu appliquer \(x) x[1L] + 1L sur -1L.

Ce calcul est exactement similaire au précédent. Ce qui est mis en avant ici, c’est que seul le type du troisième argument (FUN.VALUE) importe, sa valeur n’a aucune importance. Ici, si on veut retourner un vecteur entier, n’importe quel entier de taille 1L fait l’affaire.

Dans le cas où, comme ici, chaque application unitaire de la fonction utilisée retourne plusieurs valeurs, alors l’argument FUN.VALUE doit être un vecteur de même taille.

  • L’application de \(x) c(x, x, x) sur "a" donne c("a", "a", "a").
  • L’application de \(x) c(x, x, x) sur "b" donne c("b", "b", "b").
  • L’application de \(x) c(x, x, x) sur "c" donne c("c", "c", "c").
  • L’application de \(x) c(x, x, x) sur "d" donne c("d", "d", "d").

On attend quatre objets de taille 3L. C’est pour cela que nous soumettons à l’argument FUN.VALUE un "character" de taille 3L.

L’objet retourné sera une matrice et non plus un vecteur. Comme dans le cas vectoriel, son type est déterminé par celui de FUN.VALUE.

Là encore, on remarque que les valeurs de l’argument FUN.VALUE sont purement arbitraires. Seules sa longueur et son type importent.

Comme on le voit ci-dessous, sapply est un raccourci de vapply sans la FUN.VALUE. sapply devine automatiquement les dimensions et le type requis. Utiliser un vapply explicite est cependant en général une meilleure pratique à l’intérieur d’un programme.

sapply est plus laxiste que vapply

Essayer d’utiliser un sapply sur :

lapply(
  list(c("a", "b"),
       c("c", "d", "e"),
       c("f", "g")),
  function(x) c(x, x, x)
)

Que se passe-t-il ? Pourquoi ? Peut-on résoudre ce problème avec un vapply ?

Les vecteurs retournés par la fonction c(x, x, x) sur ses entrées sont de tailles variables. C’est rédhibitoire pour que sapply puisse déterminer une dimension de matrice adéquate. sapply laisse donc le résultat sous la forme d’une liste.

On ne peut pas résoudre ce problème structurel avec un vapply, mais vapply a le mérite d’être plus strict : il renverra une erreur si les valeurs ne sont pas de la dimension explicitement demandée. vapply est en général une meilleur pratique.

Map : appliquer une fonction multi-paramètres termes à termes

On a vu avec lapply comment appliquer une fonction sur chaque termes d’une liste ou d’un vecteur et retourner une liste. Cependant, parfois, un seul argument ne suffit pas, et l’on a envie d’appliquer une fonction terme à terme sur plusieurs listes ou vecteurs. La fonction Map remplit ce besoin.

On voit ici que la fonction Map permet d’appliquer termet à terme la fonction paste sur plusieurs vecteurs character.

L’argument MoreArgs de Map permet de rajouter une liste d’arguments complémentaires, qui restent fixes entre tous les appels.

On aurait aussi tout aussi pu utiliser une fonction anonyme pour spécifier des arguments complémentaires. C’est au choix !

On vérifie ci-dessus que la fonction Map permet d’utiliser plus que deux arguments.

La fonction Map peut tout-à-fait utiliser des listes. Ici, elle renvoie donc une liste appliquant la fonction + terme à terme :

  • c(1L, 2L, 3L) + c(10L, 11L, 12L) pour le premier terme.
  • c(4L, 5L, 6L, 7L) + c(13L, 14L, 15L, 16L) pour le deuxième terme.
  • c(8L, 9L) + c(17L, 18L) pour le troisième terme.

replicate : une variante commode de lapply pour les générations de nombres aléatoires

Certaines fonctions utiles en statistiques génèrent des nombres aléatoires. Par exemple, runif(2L) renvoie 2L nombres entre 0 et 1 selon une loi uniforme. Si on veut générer 4L vecteurs de 2L nombres aléatoires, on voudrait appliquer un lapply sur function() runif(2L).

Le problème, c’est que cette fonction est sans paramètre. Du coup comment lui spécifier un lapply de bonne longueur ? On voudrait faire un truc du genre :

Mais cela n’est pas permis par R. On peut contourner le problème avec la solution ci-dessous :

Ce n’est cependant pas des plus élégants, car on utilise un argument purement fictif. Une alternative un peu plus claire existe donc. C’est la fonction replicate.

replicate est une variante de lapply qui permet d’évaluer une expression plusieurs fois de suite. Cela n’est, bien entendu, utile que dans le cas où cette expression renvoie des résultats différents à chaque évaluation, ce qui est notamment le cas en ce qui concerne la générations de nombres aléatoires.

On note l’argument simplify = FALSE à la fin. Si celui-ci n’est pas spécifié, il est par défaut défini à la valeur TRUE, et alors replicate se comporte comme un sapply ; il essaye de construire des matrices.

Note

Puisque replicate ne prend pas une fonction pour argument mais une expression, elle n’est pas stricto censu une fonction d’ordre supérieur. On la fait tout de même figurer ici car elle reste voisine d’un lapply.

apply : appliquer une fonction sur les lignes ou les colonnes d’une matrice

apply permet d’appliquer une fonction par ligne ou par colonne sur une matrice.

Dans le tronçon de code ci-dessus, on a appliqué la fonction max (maximum) respectivement sur chaque ligne et chaque colonne. C’est l’argument MARGIN en deuxième position qui permet de déterminer la direction de cette application de fonction (1 pour les lignes, 2 pour les colonnes).

Note

Il est à noter que quelques optimisations de ces fonctions existent. rowSums, colSums, rowMeans, colMeans ont toutes les quatre des noms assez parlants qui permettent d’éviter une écriture un peu trop poussive.

do.call : appliquer une fonction multi-paramètres sur une liste d’arguments

La fonction do.call permet d’utiliser une fonction multi-paramètres, par exemple (mais pas obligatoirement) une fonction dépendant de paramètres en dots ..., sur une liste de valeurs.

Ici, on a appliqué la fonction paste en même temps sur chaque élément de la liste fournie en argument. On peut noter, également, que les éléments nommés de la liste (ici sep) sont traduits par des arguments nommés.

Reduce : réduire une liste ou un vecteur d’arguments par application successive d’un opérateur binaire

La fonction Reduce permet d’appliquer consécutivement une opération binaire.

Cet exemple (que l’on a déjà vu dans le chapitre précédent) correspond à ((1 + 2) + 8) + (-7). Chaque étape utilise l’opération binaire +.

Comme on le voit ci-dessus, on peut éventuellement, à l’aide de accumulate = TRUE, conserver les résultats intermédiaires de la réduction. À savoir ici 1, puis 1 + 2, puis 1 + 2 + 8 et enfin 1 + 2 + 8 - 7.

Mais Reduce peut être employé dans des calculs plus complexes.

Dans l’exemple précédent, on a appliqué sur une liste de fonctions l’opérateur Reduce au sens de la composition. C’est un joli exemple pour montrer la richesse d’un langage fonctionnel et illustrer l’idée que Reduce n’est pas restreint à des opérations numériques. En général, cependant, on préfèrera éviter d’empiler des fonctions non-évaluées. On préfèrera donc une notation intermédiaire comme employé ci-dessous.

Ici, plutôt que d’empiler des fonctions en mémoire, on les applique successivement. applique_fonction reste une opération binaire, mais est une loi de composition externe, dans la mesure où f est une fonction tandis que accumulateur est un résultat numérique. On remarque aussi qu’on a utilisé l’argument init de Reduce qui permet d’initialiser l’accumulateur. En effet si on veut obtenir atan(pi) / 3 + 1 il faut bien renseigner qu’on commence à pi quelque part. L’argument right = TRUE permet de parcourir la liste de droite à gauche plutôt que de gauche à droite.

Exercices

Question 1

À l’aide de la fonction pmax (sans utiliser la fonction max), calculer le maximum de cette liste. Vous pouvez consulter l’aide de la fonction pmax avec help(pmax).

liste <- list(1, -7, 8, 0)
do.call(pmax, liste)
[1] 8

Question 2

Générer une liste avec 10 vecteurs de taille 60, chacun répartis selon une loi normale d’espérance 3 et d’écart-type 0.5. Vous pouvez consulter l’aide de la fonction rnorm via help(rnorm).

replicate(10, rnorm(n=60,mean = 3,sd = 0.5), simplify = FALSE)
[[1]]
 [1] 3.026291 3.102906 3.298231 3.643193 2.175127 2.771419 3.051818 3.132905
 [9] 3.698729 3.315392 4.583561 3.476363 3.178739 3.343133 2.413688 2.925593
[17] 3.066690 2.691061 2.295140 2.630350 3.312101 2.875170 3.328890 3.005800
[25] 3.781834 2.559699 3.646359 2.866988 3.172431 2.964725 3.165792 3.553908
[33] 2.412782 3.515645 3.461900 2.490214 3.745164 2.966807 3.369774 4.039063
[41] 2.597432 2.799359 2.376498 2.842159 2.459303 3.428091 2.511917 3.074744
[49] 3.250772 3.210406 2.950579 2.727435 2.771627 2.442197 3.695375 3.310901
[57] 2.827000 2.515356 3.612799 1.703985

[[2]]
 [1] 2.896724 3.206944 2.843362 3.320457 2.601868 3.220907 3.499292 3.269855
 [9] 2.859053 2.543625 2.525291 3.081595 2.636285 2.496306 2.629806 2.665284
[17] 3.618757 3.522917 3.852183 3.316918 2.993908 2.555473 3.204100 3.161372
[25] 2.385187 2.736992 3.042042 2.372118 2.963582 3.125596 2.747101 3.585850
[33] 3.567970 3.014140 2.273346 2.615297 3.137835 3.139054 4.052988 3.199046
[41] 2.718254 2.532472 2.726467 3.431201 1.913830 2.889596 3.598505 3.191680
[49] 3.028590 4.050775 2.431253 3.577185 2.920904 2.303328 2.641124 2.797847
[57] 3.215033 2.582217 3.658790 3.618510

[[3]]
 [1] 3.880135 3.698222 2.926036 2.550648 3.778854 3.499968 2.592195 2.168213
 [9] 3.148644 3.045161 3.099317 3.030437 2.705805 3.885776 3.163740 4.591309
[17] 3.467190 2.511841 2.270769 2.993210 3.522257 2.439799 3.322735 2.591610
[25] 2.943248 2.643378 3.377344 3.364136 2.966652 2.705950 3.349759 1.980779
[33] 3.401920 3.114182 2.947678 4.414205 2.917620 3.096747 2.996612 2.365679
[41] 2.804206 2.404915 3.206559 3.319352 2.485285 2.869783 3.729283 3.052828
[49] 3.549227 2.386470 3.161813 3.333245 3.018403 3.481969 3.220861 2.232075
[57] 3.277656 2.853823 3.483073 2.331198

[[4]]
 [1] 2.645056 3.551492 2.611729 3.358073 2.707710 2.973904 3.313873 2.662288
 [9] 3.439115 2.721053 2.542890 3.048076 2.474599 3.032908 2.888408 2.980085
[17] 2.570715 2.479087 3.679426 2.048304 3.172936 2.914569 3.177198 2.689569
[25] 2.377925 2.687549 3.648891 3.548732 3.280016 3.379932 2.900018 2.693541
[33] 2.827589 3.680015 3.141088 2.692793 2.749789 3.589600 2.685566 3.210448
[41] 2.486781 2.820727 3.401477 2.888482 2.878895 3.229751 2.893468 3.929572
[49] 3.621664 3.327105 3.397446 2.552515 3.627585 3.651503 3.539554 3.309630
[57] 2.897001 2.924756 3.329863 2.745405

[[5]]
 [1] 3.072940 1.996937 3.126056 2.847241 3.183395 3.390233 2.946793 3.050003
 [9] 2.897171 3.328206 2.992530 3.362019 3.054674 2.395413 2.991793 2.447230
[17] 3.074443 2.565350 3.052146 2.707642 3.235775 3.123489 2.968255 3.421515
[25] 3.065592 2.155775 3.298144 2.443439 3.374845 3.359291 2.771467 2.846139
[33] 3.391558 2.779350 3.317196 2.651411 2.884849 2.552813 3.430447 3.181832
[41] 3.404877 2.794453 3.206064 2.353523 3.527515 3.244696 3.403568 3.379172
[49] 3.135308 3.041790 2.309449 3.248461 2.639064 3.587092 2.956539 3.119103
[57] 2.975350 2.931594 3.250955 3.058522

[[6]]
 [1] 3.576775 3.101050 3.036122 3.185620 3.339422 3.466187 2.821026 2.590394
 [9] 3.619806 3.143569 2.566983 3.990922 3.082063 3.376832 2.541360 3.329105
[17] 3.020656 3.821042 2.746648 2.574304 2.486420 3.110658 2.916255 2.620074
[25] 2.381715 2.299058 3.731210 2.873671 2.850429 2.688947 3.132912 2.929430
[33] 2.228628 3.249111 2.292049 3.519739 3.118142 2.634480 3.189305 2.794158
[41] 2.900752 2.124319 2.657727 3.137560 2.944186 3.252481 2.912668 2.266264
[49] 2.406258 1.826815 3.977109 2.869327 2.618359 3.091644 3.124207 3.212523
[57] 2.622859 2.955153 2.760575 3.631020

[[7]]
 [1] 2.682486 2.966685 4.038752 3.375590 3.474099 3.454590 3.677613 4.182802
 [9] 4.003599 3.908477 3.500731 2.856243 3.439080 2.953683 2.829001 2.646974
[17] 2.242376 2.579393 2.571987 3.199053 3.562481 3.485004 2.391125 3.710744
[25] 2.757564 3.200432 3.290129 3.741314 2.424713 2.905040 2.371317 3.792405
[33] 3.247353 2.426890 3.232258 3.203505 2.317942 2.789352 2.238672 2.924715
[41] 1.672035 2.643434 3.635583 3.278667 2.798276 2.874657 3.060512 3.808778
[49] 2.026503 2.563610 4.145919 2.585924 3.364618 3.356177 2.676804 3.643793
[57] 3.626985 3.024275 2.437894 2.393529

[[8]]
 [1] 3.334852 2.644497 3.268990 3.085764 2.275883 2.561257 2.645129 3.263871
 [9] 2.755089 2.436628 2.705974 4.559094 3.879587 2.772497 3.147112 3.536726
[17] 2.214611 3.099330 3.522167 2.600923 3.146757 2.820029 2.050962 2.928447
[25] 3.208669 2.691506 3.702209 2.347836 2.931207 2.699236 3.352114 2.440224
[33] 3.720658 3.553148 2.441886 2.840686 3.202520 2.153779 3.197497 2.757447
[41] 3.032684 1.322757 3.203036 2.808802 2.389937 2.957058 3.518527 3.796311
[49] 2.803119 3.343641 2.568480 2.774460 3.332035 2.290066 2.670998 4.011002
[57] 2.773734 2.529922 3.281395 2.488228

[[9]]
 [1] 3.810392 2.569363 3.013729 3.000244 3.731362 2.690268 2.674790 1.989182
 [9] 2.356477 2.921607 2.771036 3.124039 2.544347 3.283766 2.117975 3.476910
[17] 2.409402 3.242818 2.670452 3.344670 3.015946 2.937505 2.494066 2.535179
[25] 2.187855 3.037132 1.887367 2.964946 2.932819 2.781239 3.102431 3.065156
[33] 2.680293 2.713009 2.676390 2.954130 3.309206 2.743937 2.644528 3.063178
[41] 3.734057 2.621947 3.041481 3.195825 1.911116 2.537371 2.637637 2.811728
[49] 3.449560 3.279932 3.357073 2.935319 2.250549 2.839467 2.641778 3.717795
[57] 2.912274 2.854299 2.425850 3.294475

[[10]]
 [1] 3.587249 2.882396 3.124961 2.796840 2.394944 3.085082 1.711333 3.981324
 [9] 3.020748 2.789794 2.898419 3.690212 3.133816 3.395022 2.754824 3.007436
[17] 2.843599 3.031872 2.673660 2.040184 2.676713 3.433498 2.733029 1.926160
[25] 3.018190 2.529860 2.880954 3.276821 2.819053 2.388436 3.158916 3.144519
[33] 2.861894 2.556987 3.616170 3.537519 3.572411 3.673879 3.125004 3.684809
[41] 2.468895 3.423103 2.325115 3.619626 3.282056 2.583738 2.928843 2.395243
[49] 3.088631 3.410183 3.206972 2.866204 3.200869 2.862684 2.452599 2.363707
[57] 2.519859 3.260061 3.230654 3.251892

Question 3

À l’aide de la fonction paste0 (qui est comme la fonction paste mais avec l’argument par défaut sep = ""), produire la chaine de caractère "aabbccddeeffgg (...) xxyyzz". Les lettres en minuscules sont accessibles via letters.

do.call(paste0, Map(paste0, letters, letters))
[1] "aabbccddeeffgghhiijjkkllmmnnooppqqrrssttuuvvwwxxyyzz"
# ou

do.call(paste0, as.list(rep(letters, each = 2)))
[1] "aabbccddeeffgghhiijjkkllmmnnooppqqrrssttuuvvwwxxyyzz"
# ou plus simplement

paste0(rep(letters, each = 2), collapse = "")
[1] "aabbccddeeffgghhiijjkkllmmnnooppqqrrssttuuvvwwxxyyzz"

Question 4

Observer cette matrice des permutations en colonnes de 1L:5L :

vecteur <- 1L:5L
matrix(
  c(tail(vecteur, 5L),
    tail(vecteur, 4L), head(vecteur, 1L),
    tail(vecteur, 3L), head(vecteur, 2L),
    tail(vecteur, 2L), head(vecteur, 3L),
    tail(vecteur, 1L), head(vecteur, 4L)),
  nrow = length(vecteur),
  ncol = length(vecteur))
     [,1] [,2] [,3] [,4] [,5]
[1,]    1    2    3    4    5
[2,]    2    3    4    5    1
[3,]    3    4    5    1    2
[4,]    4    5    1    2    3
[5,]    5    1    2    3    4

Pouvez-vous généraliser cette création de matrice à n’importe quel vecteur d’entiers ? Pour ce faire, construisez une fonction super_matrice dont l’unique paramètre est vecteur.

vecteur <- 1L:5L
super_matrice <- function(vecteur) {
  longueur <- length(vecteur)
  vapply(0L:(longueur - 1L),
         \(n) c(tail(vecteur, longueur - n), head(vecteur, n)),
         rep(0L,longueur))
}
super_matrice(vecteur)
     [,1] [,2] [,3] [,4] [,5]
[1,]    1    2    3    4    5
[2,]    2    3    4    5    1
[3,]    3    4    5    1    2
[4,]    4    5    1    2    3
[5,]    5    1    2    3    4

Question 5

Proposer une version du crible d’Eratosthène faisant apparaître, pour un entier n donné, un vecteur booléen de taille n donnant la primalité (ou non) de l’entier i.

Par exemple, crible(10) donne c(FALSE, TRUE, TRUE, FALSE, TRUE, FALSE, TRUE, FALSE, FALSE, FALSE). En effet 1 n’est pas premier, 2 est premier, 3 est premier…

On pourra utiliser la fonction Reduce.

Essayer, dans un premier temps, d’obtenir une fonction est_multiple avec i pour argument et un n fixé à l’avance dans l’environnement global, un vecteur :

  • de taille n
  • valant TRUE sur les multiples de i
  • valant FALSE ailleurs
n <- 10L
est_multiple <- function(i) 1L:n %% i == 0L
est_multiple(2L)
 [1] FALSE  TRUE FALSE  TRUE FALSE  TRUE FALSE  TRUE FALSE  TRUE

Essayer, dans un premier temps, d’obtenir une fonction ni_multiple_ni_i avec i pour argument et un n fixé à l’avance dans l’environnement global, un vecteur :

  • de taille n
  • valant TRUE sur i
  • valant FALSE sur les autres multiples de i
  • valant TRUE ailleurs
n <- 10L
ni_multiple_ni_i <- function(i) 1L:n == i | 1L:n %% i != 0L
ni_multiple_ni_i(2L)
 [1]  TRUE  TRUE  TRUE FALSE  TRUE FALSE  TRUE FALSE  TRUE FALSE
crible <- function(n) {
  Reduce(`&`,
         lapply(2L:sqrt(n), \(i) 1L:n == i | 1L:n %% i != 0L),
         init = c(FALSE, rep(TRUE, n - 1L)))
}
crible(10L)
 [1] FALSE  TRUE  TRUE FALSE  TRUE FALSE  TRUE FALSE FALSE FALSE