X. Les Nombres▲
Haskell fournit une collection très complète de types numériques, basée sur le modèle de Scheme, qui découlent eux-mêmes de ceux de Common Lisp. Toutefois, ces langages sont typés dynamiquement. Les types standards contiennent déjà des entiers à précision fixé ou arbitraire, des nombres rationnels formés à partir des types entiers, des chiffres réels à simple ou double précision, et des nombres complexes à « virgule flottante ». On ne montrera que les caractéristiques de base de la structure des classes de types numériques, et le lecteur est invité à se référer à la section 6.4 pour de plus amples détails.
X-A. Structure des classes numériques▲
Les classes des types numériques (Num et celles qui en découlent) forment une grande partie des classes de la librairie standard de Haskell. On remarquera que Num est une classe de Eq, mais pas de Ord. En effet, la notion d'ordre ne s'applique pas aux nombres complexes. Cependant, la sous-classe Real héritée de Num est bien une sous-classe de Ord.
La classe Num fournit plusieurs opérations de base communes à tous les types numériques. On y trouve, entre autres, l'addition, la soustraction, la négation, la multiplication, et la valeur absolue :
(+
), (-
), (*
) ::
(Num
a) =>
a ->
a ->
a
negate, abs ::
(Num
a) =>
a ->
a
La négation est une fonction appliquée par le seul opérateur préfixe de Haskell, -, on ne peut pas l'appeler (-), car cela représente la fonction soustraction. On a donc été obligé de lui donner un nom à la place. Par exemple, -x*y est équivalent à negate (x*y). Notez que l'opérateur préfixe moins a la même priorité que l'opérateur infixe moins, qui est plus faible que celle de la multiplication.
Remarquez que Num ne fournit pas d'opération division. En effet, il y existe deux types de division qui sont fournis par des types non disjoints :
- La classe Integral fournit la division entière et le modulo. Les instances standards de Integral sont Integer, entiers au sens mathématique donc non bornés (aussi appelés « bignums »), et Int, représentant des entiers machine bornés codés sur au moins 29 bits signés. Une implémentation particulière de Haskell peut fournir d'autres types entiers en supplément. Remarquez que Integral est une sous-classe de Real, et non une sous-classe directe de Num. Cela signifie que l'on n'essaie pas de fournir des entiers de Gauss.
- Les autres types numériques découlent de Fractional, qui fournit une opération de division classique, (/). Sa sous-classe Floating contient des fonctions trigonométriques, logarithmiques et exponentielles.
- La sous-classe RealFrac héritant de Fractional et de Real fournit une fonction properFraction, qui décompose le nombre en sa partie entière et sa partie fractionnaire, et un ensemble de fonctions qui arrondissent les valeurs selon différentes règles :
properFraction ::
(Fractional
a, Integral
b) =>
a ->
(b,a)
truncate, round,
floor, ceiling: ::
(Fractional
a, Integral
b) =>
a ->
b
- La sous-classe RealFloat héritant de Floating et de RealFrac fournit quelques fonctions spécialisées pour accéder efficacement aux composantes des nombres à virgules, nommées exponent et significand.
- Les types standards Float et Double dérivent de la classe RealFloat.
X-B. Les Nombres construits▲
Parmi les types numériques standards, Int, Integer, Float et Double sont primitifs. Les autres sont construits à partir de ceux-ci, grâce à des constructeurs de type.
Complex, situé dans la librairie Complex, est un constructeur de type qui produit un type complexe de la classe Floating à partir du type RealFloat :
data
(RealFloat
a) =>
Complex a =
!
a :
+
!
a deriving
(Eq
, Text)
Les symboles ! sont des marqueurs de restriction, dont on a parlé dans la section 6.3. Remarquez que le contexte RealFloat a restreint le type de l'argument. Par conséquent, les types complexes standards sont Complex Float et Complex Double. On peut aussi voir dans la déclaration de données que le nombre complexe est écrit x :+ y, les arguments sont les parties réelles et imaginaires. Comme :+ est un constructeur de donnée, on peut l'utiliser dans la reconnaissance de motif :
conjugate ::
(RealFloat
a) =>
Complex a ->
Complex a
conjugate (x:+
y) =
x :
+
(-
y)
De la même manière, le constructeur de type Ratio, situé dans la librairie Rational, produit un type rationnel de classe RealFrac à partir d'une instance de type Integral. Au passage, il faut signaler que Rational est synonyme de Rational Integer. Cependant, Ratio est un constructeur de type abstrait. Au lieu d'utiliser un constructeur de données comme :+, les rationnels utilisent la fonction % pour créer un ratio à partir de deux entiers. Au lieu d'utiliser de la reconnaissance de motif, des fonctions d'extraction des composantes sont fournies :
(%
) ::
(Integral
a) =>
a ->
a ->
Ratio
a
numerator, denominator ::
(Integral
a) =>
Ratio
a ->
a
Pourquoi a-t-on fait une différence ? Les nombres complexes sous forme cartésienne sont uniques, et ne font pas d'égalités non triviales impliquant :+. D'autre part, les ratios ne sont pas uniques, mais possèdent une forme réduite que l'implantation du type abstrait doit conserver. Il n'est pas certain, par exemple, que numerator (x%y) soit égal à x, bien que la partie réelle de x:+y soit toujours x.
X-C. Les Conversions numériques et les surcharges de littéraux▲
Le Prélude standard et ses librairies fournissent plusieurs fonctions surchargées qui servent aux conversions explicites :
fromInteger ::
(Num
a) =>
Integer
->
a
fromRational ::
(Fractional
a) =>
Rational
->
a
toInteger ::
(Integral
a) =>
a ->
Integer
toRational ::
(RealFrac
a) =>
a ->
Rational
fromIntegral ::
(Integral
a, Num
b) =>
a ->
b
fromRealFrac ::
(RealFrac
a, Fractional
b) =>
a ->
b
fromIntegral =
fromInteger . toInteger
fromRealFrac =
fromRational . toRational
Deux d'entre elles sont implicitement utilisées pour produire des littéraux numériques surchargés : Un entier numérique (sans décimales) est effectivement équivalent à l'application de fromInteger à la valeur d'un nombre de type Integer. De la même manière, un nombre décimal est vu comme une application de fromRational à la valeur d'un nombre de type Rational. Par conséquent, 7 a le type (Num a) => a, et 7.3 a le type (Fractional a) => a. Cela signifie que l'on peut choisir d'utiliser des littéraux numériques dans des fonctions numériques génériques. Par exemple :
halve ::
(Fractional
a) =>
a ->
a
halve x =
x *
0
.5
Cette manière indirecte de surcharger des littéraux numériques a un avantage supplémentaire : la méthode qui interprétait un littéral numérique comme un nombre d'un type donné peut être spécifié dans une déclaration d'instance de la classe Integral ou Fractional, grâce aux fonctions fromInteger et fromRational. Par exemple, l'instance Num de (RealFloat a) => Complex a contient cette méthode :
fromInteger x =
fromInteger x :
+
0
Cela dit que l'instance Complex de fromInteger est définie pour produire un nombre complexe dont la partie réelle est fournie par une instance RealFloat appropriée de fromInteger. De cette manière, même les types numériques définis par l'utilisateur (ex. : les quaternions) peuvent utiliser des surcharges.
Si vous souhaitez un autre exemple, rappelez-vous de notre première définition de inc à la section 2 :
inc ::
Integer
->
Integer
inc n =
n+
1
Si l'on ignore la signature du type, le type le plus général pour inc est (Num a) => a->a. Cependant, la signature explicite du type est valide parce qu’elle est plus spécifique que le type principal. Une signature de type plus générale aurait provoqué une erreur de typage statique. La signature de type a pour effet de restreindre le type de inc, et dans ce cas aurait causé quelque chose comme inc (1::Float), qui est mal typé.
X-D. Les Types numériques par défaut▲
Considérez la définition de fonction suivante :
rms ::
(Floating
a) =>
a ->
a ->
a
rms x y =
sqrt ((x^
2
+
y^
2
) *
0
.5
)
La fonction d'exponentiation (^), qui est l'une des trois opérations dont les types sont différents fournies en standard pour l'exponentiation (cf section 6.8.5), a le type (Num a, Integral b) => a -> b -> a. Par ailleurs, étant donné que 2 a le type (Num a) => a, le type de x^2 est (Num a, Integral b) => a. Ceci pose un problème, car il n'y a pas moyen de résoudre la surcharge associée à une variable de type b. En effet, bien qu’elle soit dans le contexte, l'information a disparu du type de l'expression. Pour faire simple, le programmeur a spécifié que x devrait être élevé au carré, mais n'a pas précisé si le type de 2 était Int ou Integer. Bien entendu, cela peut se corriger :
rms x y =
sqrt ((x ^
(2
::
Integer
) +
y ^
(2
::
Integer
)) *
0
.5
)
Cependant, il est évident que de tels événements réapparaitront tôt ou tard.
En fait, ce genre d'ambiguïté sur la surcharge ne touche pas que les nombres :
show (read "xyz"
)
Sous quel type est-on censé lire la chaîne de caractères ? Ce problème peut être plus sérieux qu'avec l'exponentiation, car là-bas n'importe quelle instance de type Integral fonctionnait, alors qu'ici différents comportements peuvent être souhaités selon l'instance de Text que l'on utilisera pour lever l'ambiguïté.
En raison de la différence entre les cas numérique et quelconque des problèmes d'ambiguïté lors de la surcharge, Haskell fournit une solution qui ne s'applique qu'aux nombres. Chaque module peut contenir une déclaration « par défaut » suivie par une liste de types numériques simples (sans variables) entre parenthèses et séparés par des virgules. Quand une ambiguïté sur le type d'une variable est découverte, comme avec b, si au moins l'une des classes est numérique et que toutes ses classes sont standards, la liste par défaut sera consultée, et le premier type valide trouvé sera utilisé. Par exemple, si la déclaration par défaut est default (Int, Float), une exponentiation ambigüe sera résolue avec le type Int. (voir la section 4.3.4 pour plus de détails)
Par défaut, on utilise la liste par défaut (Integer, Double), mais (Integer, Rational, Double) peut aussi convenir. Les programmeurs très prudents peuvent préférer default (), ce qui supprime les choix par défaut.