Types de fonctions
Lorsque vous parcourez des paquets tels que elm/core
et elm/html
, vous verrez certainement des fonctions avec plusieurs flèches. Par exemple :
String.repeat : Int -> String -> String
String.join : String -> List String -> String
Pourquoi tant de flèches ? Que se passe t-il ici ??
Les parenthèses cachées
Cela devient plus clair lorsque vous voyez toutes les parenthèses. Par exemple, il est également valide d'écrire le type de String.repeat
comme ceci :
String.repeat : Int -> (String -> String)
C'est une fonction qui prend un Int
et produit ensuite une autre fonction. Voyons cela en action :
Donc, conceptuellement, chaque fonction accepte un seul argument. Elle peut renvoyer une autre fonction qui accepte un argument. Etc. À un moment donné, elle cessera de renvoyer des fonctions.
Nous pourrions toujours mettre les parenthèses pour indiquer que c'est ce qui se passe réellement, mais cela commence à devenir assez lourd lorsque vous avez plusieurs arguments. C'est la même logique que derrière l'écriture 4 * 2 + 5 * 3
au lieu de (4 * 2) + (5 * 3)
. Cela signifie qu'il y a un peu plus à apprendre, mais c'est tellement fréquent que cela en vaut la peine.
D'accord, mais à quoi sert cette fonctionnalité à la base ? Pourquoi ne pas faire (Int, String) -> String
et donner tous les arguments en même temps ?
Application partielle
La fonction List.map
est couramment utilisée dans les programmes Elm :
List.map : (a -> b) -> List a -> List b
Elle prend deux arguments, une fonction et une liste, et transforme chaque élément de la liste en utilisant cette fonction. Voici quelques exemples :
List.map String.reverse ["part","are"] == ["trap","era"]
List.map String.length ["part","are"] == [4,3]
Maintenant, rappelez-vous comment String.repeat 4
avait juste le type String -> String
? Eh bien, cela signifie que nous pouvons écrire :
List.map (String.repeat 2) ["ha","choo"] == ["haha","choochoo"]
L'expression (String.repeat 2)
est une fonction String -> String
, donc on peut l'utiliser directement. Nul besoin d'écrire (\str -> String.repeat 2 str)
.
Elm utilise également la convention selon laquelle la structure des données est toujours le dernier argument dans l'écosystème. Cela signifie que les fonctions sont généralement conçues avec cette utilisation possible à l'esprit, ce qui en fait une technique assez courante.
Maintenant, il est important de se rappeler que cela peut être surutilisé ! C'est parfois pratique et clair, mais je trouve qu'il vaut mieux l'utiliser avec modération. Je recommande donc de toujours décomposer les fonctions utilitaires de haut niveau lorsque les choses deviennent un tant soit peu compliquées. De cette façon, elles portent un nom clair, leurs arguments sont nommés et il est facile de les tester. Dans notre exemple, cela signifie créer :
-- List.map redoublement ["ha","choo"]
redoublement : String -> String
redoublement string =
String.repeat 2 string
Ce cas est vraiment simple, mais (1) il est maintenant plus clair que je m'intéresse au phénomène linguistique connu sous le nom de redoublement) et (2) ce sera assez facile d'ajouter une nouvelle logique à redoublement
au fur et à mesure que mon programme évolue. Peut-être qu'il me faudra un redoublement expressif#Redoublement_expressif) à un moment donné ?
En d'autres termes, si votre application partielle devient longue, faites-en une fonction utilitaire. Et si elle est multiligne, elle devrait absolument être transformée en une fonction utilitaire au niveau supérieur ! Ce conseil s'applique également à l'utilisation des fonctions anonymes.
Remarque : Si vous vous retrouvez avec "trop de" fonctions lorsque vous utilisez ce conseil, je vous conseille d'utiliser des commentaires tels que "-- REDOUBLEMENT" pour donner un aperçu des cinq ou dix fonctions suivantes. Comme à la vieille école ! Je l'ai montré avec les commentaires
-- UPDATE
et-- VIEW
dans les exemples précédents, mais c'est une technique générique que j'utilise dans tout mon code. Et si vous craignez que les fichiers ne deviennent trop longs avec ce conseil, je vous recommande de regarder La vie d'un fichier (en anglais) !
Pipelines
Elm a également un opérateur pipe qui repose sur une application partielle. Par exemple, supposons que nous ayons une fonction assainir
pour transformer la saisie de l'utilisateur en nombre entier :
-- AVANT
assainir : String -> Maybe Int
assainir input =
String.toInt (String.trim input)
On peut la réécrire de la sorte :
-- APRÈS
assainir : String -> Maybe Int
assainir input =
input
|> String.trim
|> String.toInt
Ainsi, dans ce "pipeline", nous transmettons la saisie à String.trim
, puis celle-ci est transmise à String.toInt
.
C'est confortable car cela permet une lecture "de gauche à droite" que beaucoup de gens aiment, mais les pipelines peuvent être surutilisés ! Lorsque vous avez trois ou quatre étapes, le code devient souvent plus clair si vous extrayez une fonction utilitaire au niveau supérieur. De cette sorte, la transformation a un nom. Les arguments sont nommés. Elle a une annotation de type. C'est beaucoup mieux auto-documenté de cette façon, et vos coéquipiers et votre vous futur l'apprécieront ! Tester la logique devient également plus facile.
Remarque : Personnellement, je préfère le
AVANT
, mais c'est peut-être simplement parce que j'ai appris la programmation fonctionnelle avec des langages sans pipeline !