Apprendre Haskell en venant de Python : Partie 2 : les Types Basiques

Typage statique avec inférence de type vs. typage dynamique : quelle différence pour l’utilisateur ?

Python est un langage à typage dynamique : l’interpréteur déduit à la volée le type des différentes variables sur lesquelles il travaille. Haskell est, lui, un langage à typage statique. Si vous avez fait autre chose que du Python, vous avez sûrement déjà vu ces déclarations de type obligatoire dans de nombreux langages à typage statique.

Ici, nous pourrions déclarer chacune de nos variables, par exemple ainsi :

a = 5 :: Int

Int correspond aux entiers bornés : contrairement à Python, le type Int ne correspond pas à une précision arbitraire. On peut alors tester avec l’interpréteur GHCi le résultat de cette ligne.

Pour connaitre le type d’une variable, vous pouvez écrire dans la console :

:t a

Et il devrait vous répondre :

a :: Int

Le (::) peut se lire : « est de type ».

Mais les concepteurs de Haskell ont fait quelque-chose de différent : le langage est à typage statique, effectivement, mais il utilise un système d’inférence de type. Cela fait que la gestion des types des différentes variables se fait de manière transparente pour le codeur, très proche de ce qui peut se passer en Python.

Mais, allez vous me dire, comment fait-il pour savoir si ma variable est un entier ou un flottant quand je met simplement :

a = 5

Vous pouvez modifier votre script, l’enregistrer et relancer l’interprétation en appuyant de nouveau sur F6. Pour relancer le script, il suffit de taper comme « commande de sortie » :r. Cela permettra de relancer votre script au lieu de fermer la console.

Si vous retentez de lui demander le type de a, alors vous aurez la réponse suivante :

a :: Integer

Integer correspond au nombre entier à précision arbitraire. Donc, n’étant pas sur du type de la variable a, il en a fait un nombre entier le plus général possible.

Et si je rajoute une virgule pour en faire un flottant ?

a = 5.0

Là, quand on teste le type, on obtient :

a :: Double

a est alors un nombre flottant double précision Double (à ne pas confondre avec les nombres flottants à simple précision Float).

Donc, vu qu’il est impossible d’additionner des entiers avec des flottants, ce petit code devrait nous renvoyer une erreur :

a = 5
b = 5.0
c = a + b

Mais ce n’est pas le cas. Pire, quand on regarde le type de a, on a alors un Double !

Quel est ce mystère ?

L’inférence de type, c’est magique !

En fait, il faut se souvenir avec l’interpréteur (ou le compilateur) Haskell, c’est qu’il est extrêmement paresseux. Il ne fera ses calculs que quand on lui demande. Donc a n’est pas un entier à la premier ligne, c’est juste 5, et on verra bien ce qu’on en fait après. De même pour b. Quand enfin on lui demande un calcul à la troisième ligne, alors il est bien obligé de choisir, et pour permettre de faire un calcul correctement, il est obligé de faire de a un flottant (ici un Double).

On peut même lui dire de faire quelque-chose comme cela explicitement :

a = 5 :: (Num x) => x
b = 5.0
c = a + b

Ce que j’ai écrit sur la première ligne, c’est que a doit être d’un type x quelconque, mais que ce type x doit être un nombre (c’est le Num). La flèche => permet de définir un contexte pour le type x. On pourrait lire cette ligne ainsi : a vaut 5 en étant du type x, x étant un nombre.

Il est ainsi préférable de réfléchir ainsi à la création de nos fonctions : non pas quel type elles vont prendre en entrées, mais quelles contraintes devront avoir les types en entrées ?

Les classes de types numériques

On a déjà vu que la classes Num, qui correspond à la classe des nombres : elle permet d’utiliser les opération mathématiques de base : l’addition (+), la multiplication (*), la soustraction (-).

Lorsqu’on peut définir la division classique, on parle alors de Fractional (/). Pour la division euclidienne (`div` pour le ratio, `mod` pour le reste), il faut des entiers Integral (nativement, cela correspond aux types Int et Integer, définis plus tôt).

Note : pour taper `, il faut utiliser la combinaison de touche alt gr + è puis espace sur un clavier azerty classique.

L’ensemble des rationnels peut être codé comme étant un ratio d’entier, et donc faire parties des Fractionnal sans être codé par des nombres à virgule. Les autres Fractionnal sont donc a priori des nombres à virgule flottants Floating (nativement Float et Double). Cela permet également de définir les quelques fonctions mathématiques de bases que l’on verra plus tard.

L’opération de mise à la puissance dépend des cas :

  • L’opérateur (^) correspond à la mise à la puissance par un entier positif d’un nombre quelconque, avec le résultat qui correspondra au type de la base.
  • l’opérateur (**) correspond à la mise à la puissance d’un flottant par un autre nombre flottant.

Suivant les cas, vous pourrez devrez utiliser l’un ou l’autre… C’est là que l’on commence à voir la petite différence entre Python et Haskell concernant le typage : on peut très difficilement utiliser un unique opérateur de puissance pour répondre à tous les cas avec un typage statique.

Pour permettre encore plus de choix dans la création des types, il y a également la notion d’ordre qui permet de distinguer parmi les différents nombres : les nombres que l’on peut ordonner sont des Real, les inversibles (Fractional) que l’on peut ordonner sont des RealFrac et les flottants que l’on peut ordonner sont des RealFloat.

Quelques autres types basiques

Les booléens (Bool)

Comme dans Python, on a une classe qui représente les booléens, et qui correspondent donc à la valeur vrai (True) ou faux (False). En particulier, c’est le résultat de tests d’égalité (==), d’inégalité (/=) [Attention, en Python, c’est (!=)], de tests d’infériorité ou de supériorité stricts (< ou >) ou large (<= ou >=).

Pour faire des opérations booléennes avec ce type, on a : (not) pour le non [comme en Python], (&&) pour le et [à la place de and en Python] et (||) pour le ou [à la place de or en Python]. Pour ceux qui ont déjà fait du C ou du Java, cela ne devrait pas les dépayser.

Les ordres (Ordering)

Les créateurs de Haskell se sont rendus compte que le résultat d’une comparaison pouvait mener à trois résultat :

  • a < b : a est plus petit que b (Less Than en anglais)
  • a = b : a est égal à b (EQual en anglais)
  • a > b : a est plus grand que b (Greater Than en anglais)

Il existe donc un type possédant trois valeurs qui correspond à ce résultat : ces 3 valeurs sont LT, EQ et GT.

Pour comparer une variable a et une variable b, vous pouvez utiliser la fonction compare :

a `compare` b

Les caractères (Char)

Les caractères sont représentés entre simple guillemet (‘) tandis que les chaînes de caractères sont entre double guillemets (« ). Contrairement à Python, les chaînes de caractères ne sont pas un type à part, mais c’est une liste de caractères. Nous verrons bientôt comment gérer les listes. Le codage des caractère est l’Unicode, comme en Python 3.

Note : tous vos fichiers doivent être encodé en UTF8. Pour plus d’explication sur pourquoi, je vous conseille : Sam et Max : L’encoding en Python, une bonne fois pour toute

Quelques autres classes

De nouvelles propriétés apparaissent naturellement, qui vont permettre de préciser les types que l’on pourra utiliser :

  • Eq x, si l’on peut définir l’égalité et la non-égalité sur le type x
  • Ord x, si l’on peut définir un ordre sur le type x
  • Show x, si l’on peut transformer la variable d’un type x en chaîne de caractère
  • Read x, si l’on peut transformer une chaîne de caractère en variable d’une chaîne x

D’autres classes un peu moins importantes :

  • Enum x, si l’on peut énumérer des valeurs du type x
  • Bounded x, si l’on peut définir des plus petites et des plus grandes valeurs du type x

Pour résumer les classes

Je vous propose un petit résumé des classes que l’on a vu sous forme graphique pour bien voir les parentés. A l’intérieur se trouve les opérateurs ou les fonctions emblématiques de la classe. Nous ne les avons pas tous vu, mais vous pouvez déjà en tester certaines sur la console GHCi.

Prelude_class

Cette image a été inspirée de Standard Haskell Classes.

Laisser un commentaire