Techniquement, une Higher Order Function est simplement une fonction qui fait au moins l'une des choses suivantes :
Dans le monde de la programmation fonctionnelle, l'usage tend à être plus restreint.
Le terme désigne généralement des fonctions telles que filter, map et reduce, qui appliquent une fonction passée en argument aux éléments d'une collection.
À ce stade du programme, on a déjà vu différentes façons d'appliquer une opération à tous les éléments d'une collection itérable, comme un Vector :
Ce concept se concentre sur les fonctions d'ordre supérieur (que l'on connaît dans tout langage fonctionnel, comme Haskell ou F#).
D'autres approches sont également possibles :
La fonction filter() prend une fonction passée en argument, dont la valeur de retour est un booléen, et l'applique à une collection.
Seuls les éléments qui renvoient true sont inclus dans la valeur de retour, qui est du même type de base que l'entrée (voir ci-dessous).
julia> filter(iseven, 1:6)
3-element Vector{Int64}:
2
4
6
# String is a collection of Chars, so String in -> String out
julia> filter(!isascii, "Hrōðgār")
"ōðā"
# tuple input -> tuple output
julia> filter(iseven, (1, 2, 3, 4, 5))
(2, 4)
Avec des tableaux multidimensionnels, filter aplatit les dimensions d'entrée et renvoie un Vector : la principale exception à toute règle sur la correspondance entre le type de sortie et le type d'entrée.
julia> m
2×3 Matrix{Int64}:
1 2 3
4 5 6
julia> filter(isodd, m)
3-element Vector{Int64}:
1
5
3
Les exemples ci-dessus utilisent des fonctions intégrées, mais l'usage de fonctions anonymes est très courant dans ce contexte.
julia> filter(x -> x % 3 == 0, 1:20)
6-element Vector{Int64}:
3
6
9
12
15
18
Il existe aussi une version en place, filter!(), comme pour beaucoup de fonctions de ce concept.
La fonction map() transforme une collection en appliquant une fonction à chaque élément.
Dans les cas simples, cela peut ressembler au broadcasting, avec une forme de sortie qui correspond à celle de l'entrée.
julia> map(√, [1, 4, 9])
3-element Vector{Float64}:
1.0
2.0
3.0
julia> map(x -> x^2 + 1, 1:4)
4-element Vector{Int64}:
2
5
10
17
julia> m
2×3 Matrix{Int64}:
1 2 3
4 5 6
julia> map(√, m)
2×3 Matrix{Float64}:
1.0 1.41421 1.73205
2.0 2.23607 2.44949
map() peut aussi opérer élément par élément sur plusieurs collections.
julia> map(*, [1, 2], [3, 4])
2-element Vector{Int64}:
3
8
D'un point de vue conceptuel, on peut y voir l'équivalent d'un zip() sur les différentes collections d'entrée, puis d'un map() sur chaque élément du résultat intermédiaire.
Ce n'est qu'une analogie approximative, qui n'implique rien quant à l'implémentation !
Comme pour zip(), les collections dont les formes ne correspondent pas sont tronquées à la ou aux dimensions de la plus petite.
Parfois, seuls les effets de bord de la fonction passée sont nécessaires, comme une écriture dans une base de données ou un push! dans un tableau.
Dans ce cas, on dispose de la fonction d'ordre supérieur foreach(), qui renvoie toujours nothing.
La fonction reduce() prend une fonction à deux arguments et l'applique à une collection, ce qui réduit le nombre de dimensions.
Cela peut sembler abstrait et déroutant, mais pense aux fonctions comme sum() ou prod() qui prennent une collection et renvoient une seule valeur.
julia> sum(1:4) # add
10
julia> prod(1:4) # multiply
24
Ces fonctions spéciales sont hautement optimisées et doivent toujours être utilisées lorsqu'elles sont disponibles.
On peut aussi citer maximum() et minimum(), les fonctions logiques all() et any(), et de nombreuses fonctions statistiques.
À titre d'illustration uniquement, considère la même fonctionnalité implémentée avec la fonction plus générique reduce() (rappelle-toi que les opérateurs infixes + et * sont en réalité des fonctions en interne).
julia> reduce(+, 1:4) # add
10
julia> reduce(*, 1:4) # multiply
24
Comme sum() et les autres fonctions d'agrégation, reduce() peut prendre un argument nommé facultatif dims, pour préciser la ou les dimensions à réduire.
julia> m
2×3 Matrix{Int64}:
1 2 3
4 5 6
julia> reduce(+, m; dims=1)
1×3 Matrix{Int64}:
5 7 9
Ce sont des exemples simples, car l'addition et la multiplication sont toutes deux commutatives (1+2 == 2+1) et associatives ( (1+2)+3 == 1+(2+3) ).
Cela est loin d'être universel ! Même des opérations aussi courantes que la soustraction et la division ne sont pas associatives.
S'ajoute à cela le problème que les erreurs de virgule flottante peuvent s'accumuler sur de grandes collections, si bien qu'une réduction de gauche à droite peut donner une réponse légèrement différente d'une réduction de droite à gauche.
Le sens de parcours de la fonction reduce de Julia dépend de l'implémentation et n'est pas garanti.
Pour contrôler explicitement le sens, il existe les fonctions foldl() et foldr(), qui commencent respectivement à la « gauche » et à la « droite » (en réalité en haut et en bas, pour un Vector).
julia> foldl(-, 1:3) # (1 - 2) - 3
-4
julia> foldr(-, 1:3) # 1 - (2 - 3)
2
Note que ces fonctions sont prévues pour des collections que l'on peut traiter comme unidimensionnelles, et renvoient un résultat scalaire.
L'utilisation d'un argument dims n'est pas prise en charge pour foldl et foldr, uniquement pour reduce.
Combiner une opération map avec un reduce est très courant dans divers domaines de la programmation.
On pourrait exécuter map de manière séquentielle, puis reduce sur une collection intermédiaire.
Cependant, c'est au mieux inefficace, et cela ne passe pas bien à l'échelle quand la collection grandit.
Il est fortement recommandé d'utiliser plutôt la fonction combinée mapreduce().
Elle peut mettre en œuvre un algorithme bien plus performant, qui entrelace les opérations map et reduce.
Le premier argument est la fonction à appliquer, le second est l'opérateur de réduction.
julia> mapreduce(x -> x^2 + 1, +, 1:3)
17
# equivalent to (2 + 5 + 10)
julia> sum(map(x -> x^2 + 1, 1:3))
17
Comme on peut s'y attendre, Julia dispose aussi des fonctions mapfoldl() et mapfoldr() pour les situations où le sens a son importance.
Nous lançons un club de fromage, qui utilisera l'apprentissage automatique pour sélectionner de nouveaux fromages à proposer à nos clients amateurs de fromage, en fonction de leur historique et de leurs goûts.
Les nouveaux membres doivent remplir un questionnaire initial afin que nous puissions recueillir quelques données de base pour commencer. On a ainsi constaté qu'il existe un sous-ensemble de clients emphatiques qui manquent de nuance dans leurs critiques. Comme cela risque de biaiser irrémédiablement un algorithme plus nuancé, un algorithme distinct a été mis en place pour répondre à leurs besoins. On te demande de fournir quelques fonctions auxiliaires pour traiter leurs données.
S'il existe sans doute différentes façons de résoudre les tâches qui suivent, chacune peut être résolue avec une seule fonction d'ordre supérieur, différente à chaque fois.
Le système de notation repose sur cinq étoiles, ce qui correspond simplement aux entiers 1:5.
Les clients emphatiques ne donnent que des notes de 1 ou 5, et nous voulons savoir si un client adopte ce comportement.
Implémente all_15() qui prend un vecteur de notes et renvoie true si toutes les notes valent 1 ou 5, et false sinon.
julia> ratings = [2, 3, 4, 4, 1];
julia> all_15(ratings)
false
julia> ratings = [1, 5, 5, 1, 5];
julia> all_15(ratings)
true
Nous devons séparer les clients les plus emphatiques des autres.
Implémente emphatics() qui prend un dictionnaire de clients et de notes.
Elle renvoie un dictionnaire similaire avec ceux qui n'utilisent que des notes de 1 ou 5 étoiles.
julia> ratings = ([2, 3, 5, 1, 1], [1, 1, 5, 5, 1], [4, 5, 5, 3, 2], [5, 5, 1, 1, 5]);
julia> names = ("c1", "c2", "c3", "c4");
julia> customers = Dict(zip(names, ratings))
Dict{String, Vector{Int64}} with 4 entries:
"c2" => [1, 1, 5, 5, 1]
"c1" => [2, 3, 5, 1, 1]
"c3" => [4, 5, 5, 3, 2]
"c4" => [5, 5, 1, 1, 5]
julia> emphatics(customers)
Dict{String, Vector{Int64}} with 2 entries:
"c2" => [1, 1, 5, 5, 1]
"c4" => [5, 5, 1, 1, 5]
Comme les clients emphatiques n'utilisent que des notes de 1 et 5, il sera plus pratique en termes de calcul de les convertir en 0 et 1.
Implémente tobinary() qui prend un vecteur de notes emphatiques.
Elle renvoie des notes binaires, où 1 a été remplacé par 0 et 5 a été remplacé par 1.
julia> ratings = [1, 1, 5, 5, 1];
julia> tobinary(ratings)
5-element Vector{Int64}:
0
0
1
1
0
Nos algorithmes prennent des Matrix en entrée, nous devrons donc transformer les données pour les mettre sous cette forme.
Implémente tobinarymatrix() qui prend un vecteur de vecteurs de notes emphatiques.
Elle renvoie une Matrix des données transformées, chaque vecteur de notes formant une ligne de la matrice.
julia> customersratings = [[1, 1, 5, 5, 1],[5, 5, 1, 1, 5]];
julia> tobinarymatrix(customersratings)
2×5 Matrix{Int64}:
0 0 1 1 0
1 1 0 0 1
Inscris-toi sur Exercism pour apprendre et maîtriser Julia avec 35 concepts128 exercices, et un vrai mentorat humain, le tout gratuitement.