Les nombres
Haskell fournit une riche collection de types numériques étant eux-mêmes basés sur le Common Lisp. (Ces langages sont cependant typés dynamiquement.) Les types standards incluent les entiers à précision fixe et arbitraire, les ratios (nombres rationnels) formés à partir de chaque type entier, et les nombres réels et complexes à virgule flottante à simple et double précision. Nous décrivons ici les caractéristiques de base de la structure de classe de type numérique.
Structure de classe numérique
Les classes de type numérique (classe Num et celles se trouvant en dessous) représentent de nombreuses classes Haskell standard. Nous notons également que Num est une sous-classe de Eq, mais pas de Ord ; ceci est dû au fait que les prédicats d'ordre ne s'appliquent pas aux nombres complexes. La sous-classe Real de Num, cependant, est également une sous-classe de Ord.
La classe Num fournit plusieurs opérations de base communes à tous les types numériques ; il s'agit notamment de l'addition, de la soustraction, de la négation, de la multiplication et de la valeur absolue :
- (+), (-), (*) :: (Num a) => a -> a -> a
- negate, abs :: (Num a) => a -> a
[negate est la fonction appliquée par le seul opérateur préfixe de Haskell, minus ; nous ne pouvons pas l'appeler (-), car c'est la fonction de soustraction, donc ce nom est fourni à la place. Par exemple, -x*y est équivalent à negate (x*y). (Le préfixe minus a la même priorité syntaxique que l'infixe minus, qui, bien sûr, est inférieure à celle de la multiplication.)]
Notez que Num ne fournit pas d'opérateur de division ; deux types différents d'opérateurs de division sont fournis dans deux sous-classes non superposées de Num :
La classe Integral fournit des opérations de division et de reste de nombres entiers. Les instances standard d'Integer sont Integer (entiers non bornés ou mathématiques, également appelés «bignums») et Int (entiers machine bornés, avec un intervalle équivalente à au moins 29 bits binaires signés). Une implémentation Haskell particulière peut fournir d'autres types intégraux en plus de ceux-ci. Notez qu'Integral est une sous-classe de Real, plutôt que de Num directement ; cela signifie qu'il n'y a aucune tentative de fournir des entiers gaussiens.
Tous les autres types numériques appartiennent à la classe Fractional, fournissant l'opérateur de division ordinaire (/). La sous-classe Floating contient des fonctions trigonométriques, logarithmiques et exponentielles.
La sous-classe RealFrac de Fractional et Real fournit une fonction properFraction, décomposant un nombre en ses parties entières et fractionnaires, et une collection de fonctions arrondissant à des valeurs entières selon des règles différentes :
- properFraction :: (Fractional a, Integral b) => a -> (b,a)
- truncate, round,
- floor, ceiling: :: (Fractional a, Integral b) => a -> b
La sous-classe RealFloat de Floating et RealFrac fournit des fonctions spécialisées pour un accès efficace aux composantes d'un nombre à virgule flottante, l'exposant et la mantisse. Les types standards Float et Double appartiennent à la classe RealFloat.
Nombres construits
Parmi les types numériques standards, Int, Integer, Float et Double sont primitifs. Les autres sont créés à partir de ceux-ci par des constructeurs de types.
Complex (présent dans la bibliothèque Complex) est un constructeur de types créant un type complexe dans la classe Floating à partir d'un type RealFloat :
- data (RealFloat a) => Complex a = !a :+ !a deriving (Eq, Text)
Les symboles ! sont des drapeaux de rigueur. Notez le contexte RealFloat a, restreignant le type de paramètre; ainsi, les types complexes standards sont Complex Float et Complex Double. Nous pouvons également voir à partir de la déclaration de données qu'un nombre complexe s'écrit x :+ y ; les paramètres sont respectivement les parties réelle et imaginaire cartésiennes. Puisque :+ est un constructeur de données, nous pouvons l'utiliser dans la recherche de motifs :
- conjugate :: (RealFloat a) => Complex a -> Complex a
- conjugate (x:+y) = x :+ (-y)
De même, le constructeur de type Ratio (présent dans la bibliothèque Rational) crée un type rationnel dans la classe RealFrac à partir d'une instance de Integral. (Rational est un synonyme de type Ratio Integer.) Ratio, cependant, est un constructeur de type abstrait. Au lieu d'un constructeur de données comme :+, les rationnels utilisent la fonction `%' pour former un ratio à partir de deux entiers. Au lieu de la recherche de motifs, des fonctions d'extraction de composantes sont fournies :
- (%) :: (Integral a) => a -> a -> Ratio a
- numerator, denominator :: (Integral a) => Ratio a -> a
Pourquoi cette différence ? Les nombres complexes sous forme cartésienne sont uniques : il n'existe pas d'identités non triviales impliquant :+. En revanche, les ratios ne sont pas uniques, mais ont une forme canonique (réduite) que l'implémentation du type de données abstrait doit conserver; ce n'est pas nécessairement le cas, par exemple, que le numérateur (x%y) soit égal à x, bien que la partie réelle de x:+y soit toujours x.
Coercitions numériques et littéraux surchargés
Le prélude standard et les bibliothèques fournissent plusieurs fonctions surchargées servant de coercitions 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 eux sont implicitement utilisés pour fournir des littéraux numériques surchargés : un nombre entier (sans point décimal) est en fait équivalent à une application de fromInteger à la valeur du nombre en tant qu'entier. De même, un nombre flottant (avec un point décimal) est considéré comme une application de fromRational à la valeur du nombre en tant que rationnel. Ainsi, 7 a le type (Num a) => a, et 7.3 a le type (Fractional a) => a. Cela signifie que nous pouvons 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 plutôt indirecte de surcharger les nombres présente l'avantage supplémentaire que la méthode d'interprétation d'un nombre comme un nombre d'un type donné peut être spécifiée dans une déclaration d'instance Integral ou Fractional (puisque fromInteger et fromRational sont respectivement des opérateurs de ces classes). Par exemple, l'instance Num de (RealFloat a) => Complex a contient cette méthode :
- fromInteger x = fromInteger x :+ 0
Cela signifie qu'une 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 (par exemple, les quaternions) peuvent utiliser des nombres surchargés.
Comme autre exemple, rappelons notre première définition de inc :
- inc :: Integer -> Integer
- inc n = n+1
En ignorant la signature de type, le type le plus général de inc est (Num a) => a->a. La signature de type explicite est cependant légale, car elle est plus spécifique que le type principal (une signature de type plus générale provoquerait une erreur statique). La signature de type a pour effet de restreindre le type de inc, et dans ce cas, cela provoquerait un type incorrect de quelque chose comme inc (1::Float).
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 (^) (l'un des trois opérateurs d'exponentiation standard différents avec des typages différents, a le type (Num a, Integral b) => a -> b -> a, et comme 2 a le type (Num a) => a, le type de x^2 est (Num a, Integral b) => a. C'est un problème ; il n'y a aucun moyen de résoudre la surcharge associée à la variable de type b, puisqu'elle est dans le contexte, mais a par ailleurs disparu de l'expression de type. Essentiellement, le programmeur a spécifié que x doit être au carré, mais n'a pas spécifié s'il doit être au carré avec une valeur Int ou Integer de deux. Bien sûr, nous pouvons résoudre ce problème :
- rms x y = sqrt ((x ^ (2::Integer) + y ^ (2::Integer)) * 0.5)
Il est évident que ce genre de chose va vite devenir lassant.
En fait, ce type d'ambiguïté de surcharge ne se limite pas aux nombres :
- show (read "xyz")
De quel type la chaîne est-elle censée être lue ? C'est plus grave que l'ambiguïté d'exponentiation, car là, n'importe quelle instance d'Integral fera l'affaire, alors qu'ici, on peut s'attendre à un comportement très différent selon l'instance de Text utilisée pour résoudre l'ambiguïté.
En raison de la différence entre les cas numériques et généraux du problème d'ambiguïté de surcharge, Haskell fournit une solution se limitant aux nombres : chaque module peut contenir une déclaration par défaut, composée du mot-clef default suivi d'une liste entre parenthèses et séparée par des virgules de monotypes numériques (types sans variables). Lorsqu'une variable de type ambiguë est découverte (comme b, ci-dessus), si au moins une de ses classes est numérique et toutes ses classes sont standard, la liste par défaut est consultée et le premier type de la liste qui satisfera le contexte de la variable de type est utilisé. Par exemple, si la déclaration par défaut default (Int, Float) est en vigueur, l'exposant ambigu ci-dessus sera résolu comme type Int.
La valeur par défaut est (Integer, Double), mais (Integer, Rational, Double) peut également être appropriée. Les programmeurs très prudents préféreront peut-être default(), ne fournissant aucune valeur par défaut.