Les modules
Un programme Haskell est constitué d'une collection de modules. Un module en Haskell a pour double objectif de contrôler les espaces de noms et de créer des types de données abstraits.
Le niveau supérieur d'un module contient l'une des diverses déclarations que nous avons abordées : déclarations de fixité, déclarations de données et de types, déclarations de classes et d'instances, signatures de types, définitions de fonctions et liaisons de motifs. À l'exception du fait que les déclarations d'importation (étant décrites sous peu) doivent apparaître en premier, les déclarations peuvent apparaître dans n'importe quel ordre (la portée de niveau supérieur est mutuellement récursive).
La conception des modules de Haskell est relativement conservatrice : l'espace de noms des modules est complètement plat et les modules ne sont en aucun cas de «première classe». Les noms de modules sont alphanumériques et doivent commencer par une lettre majuscule. Il n'y a pas de lien formel entre un module Haskell et le système de fichiers le prenant (généralement) en charge. En particulier, il n'y a pas de lien entre les noms de modules et les noms de fichiers, et plusieurs modules pourraient éventuellement résider dans un seul fichier (un module peut même s'étendre sur plusieurs fichiers). Bien sûr, une implémentation particulière adoptera très probablement des conventions qui rendent la connexion entre les modules et les fichiers plus stricte.
Techniquement parlant, un module n'est en réalité qu'une grande déclaration commençant par le mot clef module ; voici un exemple pour un module dont le nom est Tree :
- module Tree ( Tree(Leaf,Branch), fringe ) where
-
- data Tree a = Leaf a | Branch (Tree a) (Tree a)
-
- fringe :: Tree a -> [a]
- fringe (Leaf x) = [x]
- fringe (Branch left right) = fringe left ++ fringe right
Le type Tree et la fonction fringe devraient vous être familiers. [En raison du mot-clef where, layout est actif au niveau supérieur d'un module, et donc les déclarations doivent toutes s'aligner dans la même colonne (généralement la première). Notez également que le nom du module est le même que celui du type ; cela est autorisé.]
Ce module exporte explicitement Tree, Leaf, Branch et fringe. Si la liste d'exportation suivant le mot-clef module est omise, tous les noms liés au niveau supérieur du module seraient exportés. (Dans l'exemple ci-dessus, tout est explicitement exporté, donc l'effet serait le même.) Notez que le nom d'un type et ses constructeurs doivent être regroupés, comme dans Tree(Leaf,Branch). En raccourci, nous pourrions également écrire Tree(..). L'exportation d'un sous-ensemble des constructeurs est également possible. Les noms d'une liste d'exportation n'ont pas besoin d'être locaux au module exportateur ; tout nom dans la portée peut être répertorié dans une liste d'exportation.
Le module Tree peut maintenant être importé dans un autre module :
- module Main (main) where
- import Tree ( Tree(Leaf,Branch), fringe )
-
- main = print (fringe (Branch (Leaf 1) (Leaf 2)))
Les différents éléments importés et exportés d'un module sont appelés entités. Notez la liste d'importation explicite dans la déclaration d'importation; l'omettre entraînerait l'importation de toutes les entités exportées depuis Tree.
Noms qualifiés
Il y a un problème évident avec l'importation de noms directement dans l'espace de noms d'un module. Que se passe-t-il si deux modules importés contiennent des entités différentes portant le même nom ? Haskell résout ce problème en utilisant des noms qualifiés. Une déclaration d'importation peut utiliser le mot-clef skilled pour que les noms importés soient préfixés par le nom du module importé. Ces préfixes sont suivis du caractère «.» sans espace intermédiaire. [Les qualificateurs font partie de la syntaxe lexicale. Ainsi, A.x et A . x sont assez différents : le premier est un nom qualifié et le second une utilisation de la fonction infixe «.» .] Par exemple, en utilisant le module Tree présenté ci-dessus :
- module Fringe(fringe) where
- import Tree(Tree(..))
-
- fringe :: Tree a -> [a] -- A different definition of fringe
- fringe (Leaf x) = [x]
- fringe (Branch x y) = fringe x
-
- module Main where
- import Tree ( Tree(Leaf,Branch), fringe )
- import qualified Fringe ( fringe )
-
- main = do print (fringe (Branch (Leaf 1) (Leaf 2)))
- print (Fringe.fringe (Branch (Leaf 1) (Leaf 2)))
Certains programmeurs Haskell préfèrent utiliser des qualificateurs pour toutes les entités importées, rendant la source de chaque nom explicite à chaque utilisation. D'autres préfèrent les noms courts et n'utilisent des qualificateurs que lorsque cela est absolument nécessaire.
Les qualificateurs sont utilisés pour résoudre les conflits entre différentes entités qui ont le même nom. Mais que se passe-t-il si la même entité est importée de plusieurs modules ? Heureusement, de tels conflits de noms sont autorisés : une entité peut être importée par différentes routes sans conflit. Le compilateur sait si les entités de différents modules sont réellement les mêmes.
Types de données abstraits
Outre le contrôle des espaces de noms, les modules fournissent le seul moyen de créer des types de données abstraits (ADT) en Haskell. Par exemple, la caractéristique d'un ADT est que le type de représentation est caché ; toutes les opérations sur l'ADT sont effectuées à un niveau abstrait ne dépendant pas de la représentation. Par exemple, bien que le type Tree soit suffisamment simple pour que nous ne le rendions normalement pas abstrait, un ADT approprié pour lui pourrait inclure les opérations suivantes :
- data Tree a -- juste le nom du type
- leaf :: a -> Tree a
- branch :: Tree a -> Tree a -> Tree a
- cell :: Tree a -> a
- left, right :: Tree a -> Tree a
- isLeaf :: Tree a -> Bool
Un module prenant en charge ceci est :
- module TreeADT (Tree, leaf, branch, cell,
- left, right, isLeaf) where
-
- data Tree a = Leaf a | Branch (Tree a) (Tree a)
-
- leaf = Leaf
- branch = Branch
- cell (Leaf a) = a
- left (Branch l r) = l
- right (Branch l r) = r
- isLeaf (Leaf _) = True
- isLeaf _ = False
Notez que dans la liste d'exportation, le nom de type Tree apparaît seul (c'est-à-dire sans ses constructeurs). Ainsi, Leaf et Branch ne sont pas exportés, et la seule façon de construire ou de démonter des arbres en dehors du module est d'utiliser les différentes opérations (abstraites). Bien sûr, l'avantage de cette dissimulation d'informations est que nous pourrions ultérieurement modifier le type de représentation sans affecter les utilisateurs du type.
Autres fonctionnalités
Voici un bref aperçu de certains autres aspects du système de modules. Consultez le rapport pour plus de détails :
- Une déclaration d'importation peut masquer de manière sélective des entités à l'aide d'une clause de masquage dans la déclaration d'importation. Cela est utile pour exclure explicitement les noms qui sont utilisés à d'autres fins sans avoir à utiliser des qualificateurs pour d'autres noms importés du module.
- Une importation peut contenir une clause as pour spécifier un qualificateur différent du nom du module d'importation. Cela peut être utilisé pour raccourcir les qualificateurs des modules avec des noms longs ou pour s'adapter facilement à un changement de nom de module sans modifier tous les qualificateurs.
- Les programmes importent implicitement le module Prelude. Une importation explicite du Prelude remplace l'importation implicite de tous les noms de Prelude. Ainsi :
- import Prelude hiding length
- Les déclarations d'instance ne sont pas explicitement nommées dans les listes d'importation ou d'exportation. Chaque module exporte toutes ses déclarations d'instance et chaque importation met toutes les déclarations d'instance dans la portée.
- Les méthodes de classe peuvent être nommées soit à la manière des constructeurs de données, entre parenthèses après le nom de la classe, soit comme des variables ordinaires.
n'importera pas la longueur du prélude standard, ce qui permet de définir différemment la longueur du nom.
Bien que le système de modules de Haskell soit relativement conservateur, il existe de nombreuses règles concernant l'importation et l'exportation de valeurs. La plupart d'entre elles sont évidentes : par exemple, il est illégal d'importer deux entités différentes ayant le même nom dans la même portée. D'autres règles ne sont pas aussi évidentes : par exemple, pour un type et une classe donnés, il ne peut y avoir plus d'une déclaration d'instance pour cette combinaison de type et de classe n'importe où dans le programme.