// BLOG
Elm #4 - Fonctions
Dans cette partie 4 nous attaquons le nerd de la guerre de la programmation fonctionnelle, j'ai nomé "les fonctions". Nous allons voir les différents types de fonctions et comment les manipuler.
Les fonctions nommées
Une fonction nommée est une fonction que l’on peut appeler à plusieurs reprises dans son programme grâce au nom attribué lors de sa déclaration.
Signature et déclaration d’une fonction nommée :
-- Les fonctions commencent par une minuscule
add : Int -> Int -> Int
add a b =
a + b
La première ligne correspond à la signature de la fonction, elle n’est pas obligatoire, mais conseillée pour faciliter la compréhension du code. La signature indique le type des paramètres et de la valeur retournée. Le dernier type correspond toujours à celui de la valeur de retour, les autres types correspondent à ceux des paramètres de la fonction.
La seconde ligne déclare la fonction. On indique son nom puis ses paramètres et enfin son implémentation qui doit se conformer à ce qui est indiqué dans la signature.
Appel d’une fonction nommée :
add 1 3
> 4
En Elm, une fonction qui ne prend pas de paramètre est une constante :
user =
"Eva"
Les fonctions anonymes
\a b -> a + b
La fonction ne possède pas de nom, les paramètres sont indiqués entre l’anti-slash et la flèche. On dit quoi faire des paramètres à droite de la flèche.
Grouper les fonctions
Vous avez remarqué que les fonctions Elm n’utilisent pas de parenthèses pour définir leurs paramètres. En revanche, les parenthèses sont utilisées lorsque l’on souhaite appeler en paramètre d’une fonction le résultat d’une autre fonction.
Exemple:
addition : Int -> Int -> Int
addition a b =
a + b
division : Float -> Float -> Float
division x y =
x / y
addition 5 ( division 10 2 )
> 10
Explication :
Nous avons défini deux fonctions “addition” et “division”. Ensuite nous appelons la fonction “addition” en lui passant en deuxièmes paramètres le résultat de la fonction “division”. Pour indiquer à Elm que nous souhaitons passer en deuxièmes paramètres le résultat de “division 10 2” et non le mot “division”, nous utilisons les parenthèses.
Grouper les fonctions avec l'opérateur Pipe "|"
En Elm (langage fonctionnel), nous travaillons beaucoup avec les fonctions et grouper plusieurs fonctions est une opération fréquente. La syntaxe avec les parenthèses vue ci-dessus peut vite devenir difficile à lire lorsque l’on groupe plus de deux fonctions.
L’opérateur Pipe “|” permet de rendre l’écriture plus lisible. On l’utilise pour indiquer le passage du résultat d’une fonction à une autre.
Le pipe peut s’écrire “|>” (pipe forward) ou “<|” (pipe backward) selon la façon dont on passe les données.
Définissons trois fonctions :
-- Signature & déclaration des fonctions
addition : Int -> Int -> Int
addition a b =
a + b
division : Float -> Float -> Float
division x y =
x / y
multiplication : Int -> Int -> Int
multiplication c d =
c * d
Nous allons grouper ces trois fonctions pour effectuer un calcul, convertir le résultat en String et passer ce String à la fonction “main” (qui est le point d’entré de toute application Elm) via la fonction text qui affichera le résultat sur l’écran de l’utilisateur.
Exemple d’écriture avec les parenthèses et le pipe forward :
-- Ecriture avec les parenthèses
main =
Html.text ( String.fromInt ( addition 5 ( multiplication 10 ( division 30 10 ) ) ) )
-- Ecriture avec le pipe forward
main =
division 30 10 |> multiplication 10 |> addition 5 |> String.fromInt |> Html.text
-- ... mais on écrit généralement avec des retours à la ligne
main =
division 30 10
|> multiplication 10
|> addition 5
|> String.fromInt
|> Html.text
Nous allons grouper ces trois fonctions pour effectuer un calcul, convertir le résultat en String et passer ce String à la fonction “main” (qui est le point d’entré de toutOn voit bien ici l’idée avec l’écriture sur une seule ligne :
Je commence par diviser 30 par 10 et je passe le résultat en paramètre à la fonction multiplication qui multiplie ce résultat par 10, le résultat de cette opération est lui-même transmis en paramètre à la fonction addition qui lui ajoute 5 … et ainsi de suite.
Par convention d’écriture, on privilégiera le retour à la ligne avec le pipe forward en début de ligne.
L’avantage de cette écriture est la lisibilité et la facilité avec laquelle je peux supprimer un des calculs. Par exemple si je souhaite supprimer l’addition, je supprime simplement la ligne correspondante et je ne m’occupe pas de gérer le micmac des parenthèses ouvrantes/fermantes.
Exemple d’écriture avec les parenthèses et le pipe backward :e application Elm) via la fonction text qui affichera le résultat sur l’écran de l’utilisateur.
Exemple d’écriture avec les parenthèses et le pipe forward :
-- Ecriture avec les parenthèses
main =
Html.text ( String.fromInt ( add 5 ( multiply 10 ( divide 30 10 ) ) ) )
-- Ecriture avec le pipe backward
main =
Html.text <|
String.fromInt <|
addition 5 <|
multiplication 10 <|
divide 30 10
Le résultat et le calcul sont les mêmes mais l’écriture différente. A vous de choisir le mode d’écriture qui vous convient le mieux.
Le pipe backward peut aussi être utilisé dans des cas simples:
-- Ecriture d'un cas simple avec les parenthèses
addition 5 ( multiplication 3 4 )
-- Ecriture de la même chose avec le pipe backward
addition 5 <| multiplication 3 4
Tout ceci est possible parce que Elm autorise l’application partielle de fonctions.
Application partielle de fonctions
La première fois que j’ai lu des explications sur ce concept, je suis resté dubitatif et j’ai du relire les explications plusieurs fois. Donc si ça vous fait le même effet, don’t panic et relisez si besoin ;-)
Avec Elm, toute fonction est capable de prendre un seul paramètre et de renvoyer une fonction ou un résultat.
Si on prend le cas de notre fonction addition:
addition : Int -> Int -> Int
addition a b =
a + b
addition 2 3
> 5
Elle prend deux paramètres, les additionne et renvoie un entier comme résultat, ici “5”.
Si je passe un seul paramètre à la fonction addition
, Elm, plutôt que de renvoyer une erreur, renvoie une fonction partielle qui attend le paramètre restant pour enfin renvoyer le résultat sous forme d’un entier.
Reprenons calmement:
D’abord, addition 2
est évalué. L’argument 2
est passé à addition
qui renvoie une fonction intermédiaire attendant qu’on lui passe le second paramètre.
-- Fonction intermédiaire
addition 2
Affectons cette fonction intermédiaire à une variable addition_2
:
addition_2 = addition 2
Si on évalue addition_2
dans Elm repl, il nous retourne une fonction (partielle) qui prend en paramètre un entier Int et renvoit un entier Int :
addition_2 = addition 2
-- : Int -> Int
En passant le second paramètre de valeur 3 à cette fonction partielle, on obtient notre résultat final attendu sous forme d’un entier:
addition_2 3
> 5
Les écritures suivantes sont donc équivalentes :
-- Ceci ...
addition 2 3
-- ... équivaut à ceci
addition_2 3
-- ... équivaut à ceci
( ( addition 2 ) 3 )
-- on peut donc conclure que
addition 2 3 = ( ( addition 2 ) 3 )
-- ... et renvoie 5
> 5 : Int
Remarque : Les parenthèses sont facultatives car l’évaluation des fonctions est associée à gauche par défaut.
Elm nous permet donc de remplacer une fonction qui prend plusieurs paramètres par plusieurs fonctions partielles qui attendraient 1 seul paramètre.
Ok mais à quoi ça sert ce concept de fonctions partielles ?
La première utilité c’est que ce concept nous permet d’utiliser l’écriture à l’aide des pipes “|”. Voir le chapitre sur les pipes forward/backward ci-dessus.
La deuxième utilité est de permettre d’écrire un code plus concis.
Exemple:
(traduction partielle de l’exemple décrit dans l’article écrit par @matt24ray )
Commençons par écrire une fonction qui renvoie le double d’un nombre :
double n = n * 2
-- : number -> number
Double est de type “function”, prend un paramètre de type “number” et renvoie une valeur de type “number”.
Dans Elm tout est fonction donc ici, même l’opérateur de multiplication “*” est une fonction.
(*)
-- : number -> number -> number
On voit ici que si on évalue le résultat de la fonction (multiplication) dans Elm repl, il nous indique que l’opérateur de multiplication “” est bien une “function” et qu’il prend en paramètre deux paramètres de type “number” et renvoie une valeur de type “number”.
Grâce au concept de fonction partielle, on peut écrire la même fonction de façon plus concise :
double = (*) 2
-- : number -> number
On remarque ici que nous n’indiquons pas après le mot “double” de nom de paramètre.
Parce que (*) prend deux nombres comme paramètres, le compilateur en déduira par défaut que notre fonction “double” prend un nombre comme seul paramètre.
Créons maintenant une fonction qui double la valeur de tous les nombres d’une liste.
doubleList list = List.map double list
-- : List number -> List number
La fonction “List.map” parcours une liste d’éléments (ici “list”) et leurs applique le traitement d’une fonction (ici “double”). Elle renvoie une valeur de type “List” contenant le résultat des calculs sur chaque élément de la liste initiale.
Si on applique le même principe que l’exemple ci-dessus, on peut réecrire “doubleList” de façon plus concise :
doubleList = List.map double
-- : List number -> List number
Parce que (List.map) prend une liste en paramètre, le compilateur en déduira par défaut que notre fonction “doubleList” prend une liste comme seul paramètre.
Les variables de type
Les variables de type permettent de rendre générique les paramètres passés à une fonction. Cela signifie que la fonction peut accepter par exemple des paramètres de type String une fois et de type Int une autre fois.
switch : ( a, b ) -> ( b, a )
switch ( x, y ) =
( y, x )
La fonction switch
prend en paramètre un tuple de type a, b et retourne un tuple de type b, a
Remarque : n’importe quel nom de variable en minuscules peut être utilisé pour une variable de type, a et b ne sont que des conventions.
Tous ces appels sont valides :
-- Int Int
switch (1, 2)
-- String Int
switch ("A", 2)
-- Int List.String
switch (1, ["B"])
Fonctions en tant que paramètres
Il est possible de passer en paramètre d’une fonction une autre fonction.
map : (Int -> String) -> List Int -> List String
Ici le premier paramètre est toute fonction prenant un paramètre de type Int et renvoyant une valeur de type String. La fonction String.fromInt
répond à cette exigence et peut donc être utilisée dans ce cas.
map : (Int -> String) -> List Int -> List String
map String.fromInt [1,2,3,4]
> ["1","2","3","4"]
Si on souhaite moins de contrainte au niveau du type pour le premier paramètre, on peut utiliser les variables de type et signer la fonction de la façon suivante :
map : (a -> b) -> List a -> List b
Dans ce cas, le premier paramètre accepte toute fonction prenant un paramètre (qlq. soit le type) et renvoyant une valeur (qlq. soit le type).
Cette fonction transforme une liste de a
en liste de b
. Nous nous fichons de ce que a
et b
représentent, à partir du moment où la fonction passée comme premier paramètre utilise ces mêmes types.
convertStringToInt : String -> Int
map : (a -> b) -> List a -> List b
map convertStringToInt ["Hello", "1"]
> [5, 1]
convertIntToString : Int -> String
map : (a -> b) -> List a -> List b
map convertIntToString [1, 2]
> ["1", "2"]
Sources:
https://elm-tutorial.org//fr/01-fondations/02-fonctions.html
https://dev.to/matt24ray/partial-application-of-functions-in-elm-ll
Disclaimer:
Etant un total débutant dans le langage Elm et la programmation fonctionnel, il se peut que des incompréhensions ou des erreurs se soient glissées dans mes explications. Si vous en remarquez, merci de me les signaler pour que je puisse les corriger au plus vite.