Lire les types
Au chapitre Bases du langage, nous avons pu manipuler un certain nombre d'exemples interactifs pour prendre contact avec le langage. Poursuivons cette démarche en nous interrogeant cette fois sur le type des valeurs manipulées.
Types primitifs et listes
Entrons quelques expressions simples et observons ce qui en résulte :
Cliquez sur cette boîte noire juste au-dessus ⬆️ ; le curseur devrait commencer à clignoter. Saisissez 3.1415
et appuyez sur la touche Entrée de votre clavier. Cela devrait afficher 3.1415
suivi du type Float
.
Que se passe t-il concrètement ici ? Chaque entrée affiche une valeur suivie de son type. Vous pouvez lire ces lignes à haute voix :
- La valeur
"hello"
est uneString
(une chaîne de caractère). - La valeur
False
est unBool
(un booléen). - La valeur
3
est unInt
(un nombre entier). - La valeur
3.1415
est unFloat
(un nombre à virgule flottante).
Elm est capable de deviner le type de n'importe quelle valeur que vous lui envoyez ! Regardons ce que ça donne avec les listes :
Vous pouvez lire ces types ainsi :
- Nous avons une
List
remplie de valeurs de typeString
. - Nous avons une
List
remplie de valeurs de typeFloat
.
Au final, un type est la description sommaire du contenu d'une valeur.
Fonctions
Regardons le type de quelques fonctions :
Essayez d'entrer round
or sqrt
pour observer d'autres types ⬆️
La fonction String.length
a un type String -> Int
. Cela signifie qu'elle doit prendre un argument de type String
et qu'elle retourne une valeur de type Int
. Essayons de lui fournir un argument :
Donc on prend une fonction String -> Int
, on lui passe un argument String
, et ça donne un Int
.
Mais que se passe t-il quand on donne autre chose qu'une String
? Essayez d'entrer String.length [1,2,3]
ou String.length True
pour voir ce que ça donne ⬆️
Vous allez découvrir qu'une fonction String -> Int
doit absolument recevoir un argument de type String
!
Note: Plus Les fonctions prennent d'arguments, plus elles contiennent de flèches (
->
). Par exemple, cette fonction prend deux arguments :[ { "input": "String.repeat", "value": "\u001b[36m<function>\u001b[0m", "type_": "Int -> String -> String" } ]Fournir deux arguments à
String.repeat
commeString.repeat 3 "ha"
produira"hahaha"
. On peut retenir que->
est une façon un peu étrange de séparer les arguments, mais nous expliquons tout le raisonnement derrière ici. Et c'est plutôt cool !
Annotations de type
Jusqu'ici nous avons laissé Elm deviner les types, mais on peut également fournir une annotation de type au-dessus de la ligne définissant une fonction, comme ceci :
half : Float -> Float
half n =
n / 2
-- half 256 == 128
-- half "3" -- error!
hypotenuse : Float -> Float -> Float
hypotenuse a b =
sqrt (a^2 + b^2)
-- hypotenuse 3 4 == 5
-- hypotenuse 5 12 == 13
checkPower : Int -> String
checkPower powerLevel =
if powerLevel > 9000 then "It's over 9000!!!" else "Meh"
-- checkPower 9001 == "It's over 9000!!!"
-- checkPower True -- error!
Ajouter des annotations de type n'est pas obligatoire, mais c'est fortement recommandé ! Parmi leurs nombreux bénéfices :
- Qualité des messages d'erreur — Quand vous ajoutez une annotation de type, le compilateur comprend ce que vous essayez de faire. Votre implémentation peut comporter des erreurs, mais le compilateur peut maintenant les comparer à votre intention initiale. “Vous avez dit que
powerLevel
était unInt
, mais il est utilisé comme uneString
!” - Documentation — Quand vous revenez sur une base de code ancienne (ou quand d'autres collègues la découvrent pour la première fois), c'est très pratique de lire directement ce qui rentre et sort d'une fonction, sans avoir à lire l'implémentation très attentivement.
Il est toutefois possible de se tromper en écrivant des annotations… du coup, que se passe t-il si une annotation ne correspond pas à son implémentation ? Le compilateur infère tous les types et vérifie que votre annotation colle systématiquement à la réalité. En d'autres termes, le compilateur vérifie en permanence que toutes les annotations que vous ajoutez sont cohérentes. Ainsi, vous disposez des meilleurs messages d'erreur possibles et d'une documentation toujours à jour !
Variables de type
En lisant du code Elm, vous pouvez tomber sur des annotations comportant une ou plusieurs lettres en minuscule, comme par exemple pour la fonction List.length
:
Vous voyez la lettre a
dans le type List a -> Int
? C'est une variable de type, qui peut varier en fonction de l'usage fait de List.length
:
Nous ne nous intéressons qu'à la longueur de ces listes, sans jamais nous soucier du type de données qu'elles contiennent. La variable de type a
indique qu'on peut cibler n'importe quel type. Regardons un autre exemple courant :
À nouveau, la variable de type a
peut varier en fonction de comment List.reverse
est utilisée. Mais ici, nous avons un a
dans l'argument et le résultat. Cela signifie que quand vous passez une List Int
, vous récupérez une List Int
en retour également. Une fois décidé à quoi correspond la variable de type a
, le type sous-jacent doit être cohérent partout.
Note : Les variables de type doivent commencer par un caractère minuscule, mais elles peuvent tout aussi bien être des mots entiers. Nous pourrions écrire le type de
List.length
avec une signatureList value -> Int
et celui deList.reverse
avecList element -> List element
. Aucun problème tant qu'on commence bien par une lettre minuscule. Les variables de typea
etb
sont souvent utilisées par convention, mais certaines annotations bénéficient aussi de noms plus appropriés.
Variables de type contraintes
Il y a une variante spéciale de variables de type en Elm appelées variables de type contraintes. Une des plus courantes est number
, qu'utilisent de nombreuses fonctions comme negate
par exemple :
Essayez de soumettre des expressions comme negate 3.1415
ou negate (round 3.1415)
, puis negate "coucou"
⬆️
Normalement, les variables de type peuvent être remplies avec n'importe quel type, mais number
ne peut l'être qu'avec Int
ou Float
. Ici la variable number
contraint les possibilités.
La liste complète des variables de type contraintes est :
number
, qui autoriseInt
etFloat
appendable
, qui autoriseString
etList a
comparable
, qui autoriseInt
,Float
,Char
,String
, et les listes/tuples decomparable
compappend
, qui autoriseString
etList comparable
Ces variables de type contraintes existent pour rendre certains opérateurs comme (+)
et (<)
un peu plus flexibles.
Nous avons vu les types pour les valeurs et les fonctions plutôt exhaustivement, mais à quoi ça ressemble quand on commence à vouloir des structures de données plus complexes ?