XI. Les Modules▲
Un programme Haskell est constitué d'un ensemble de modules. En Haskell, un module a une double utilité : il sert à contrôler les espaces de nommage, et à créer un type de données abstrait.
Le niveau supérieur d'un module contient toutes les déclarations dont nous avons parlé jusqu'ici : déclarations de fixité, déclarations de type et de données, déclarations de classes et d'instances, signatures de types, définitions de fonctions, et liens avec des motifs. Excepté le fait que les déclarations d'importation (que nous déjà décrites brièvement) doivent apparaître au début, les déclarations peuvent apparaître dans n'importe quel ordre, la portée du niveau supérieur étant mutuellement récursive.
La conception de modules en Haskell est relativement conservatrice : l'espace de nommage des modules est complètement plat, et les modules ne sont aucunement des classes. Les noms des modules sont alphanumériques et doivent commencer par une lettre en majuscule. Il n'y a pas de liens directs entre un module Haskell et le fichier système qui le contient. En particulier, il n'y a aucune correspondance entre les noms de modules et les noms des fichiers, plus d'un module peut être stocké dans un seul fichier, ou un module peut résider dans plusieurs fichiers. Bien entendu, une implantation particulière adoptera certainement des conventions reliant modules et fichiers de manière plus contraignante.
Techniquement parlant, un module n'est qu'une grosse déclaration qui commence avec le mot-clé module. Voici l'exemple d'un module nommé 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 familières, car ils ont été donnés en exemple dans la section 2.2.1. En raison du mot-clé where, la mise en forme est active dès le début du module, et par conséquent les déclarations doivent toutes être alignées sur la même colonne (typiquement la première). Ainsi on remarque que le nom du module est le même que celui du type, ce qui est autorisé.
Le module exporte explicitement Tree, Leaf, Branch et fringe. Si la liste des exportations suivant le mot-clé module est omis, tous les noms liés au niveau supérieur du module seront exportés. Dans l'exemple donné, tout est explicitement exporté, ce qui a le même effet. Remarquez que le nom du type et de son constructeur ont été groupés ensemble, comme dans Tree(Leaf,Branch). On peut aussi utiliser un raccourci en écrivant Tree(…). Exporter un sous-ensemble de constructeurs est ainsi possible. Les noms dans la liste des exportations doivent ne pas être locaux au module exporté. N'importe quel nom dans la portée peut être placé dans la liste des exportations.
Le module Tree peut désormais ê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 divers éléments qui vont être importés dans le module et exportés hors de celui-ci sont appelés des entités. Remarquez la liste des importations explicite dans la liste des déclarations, sans quoi toutes les entités exportées de Tree auraient été importées.
XI-A. Les Noms qualifiés▲
Il y a un problème évident dans l'importation des noms directement dans l'espace de nommage du module. Que se passerait-il si deux modules importés contenaient des entités différentes ayant le même nom ? Une déclaration d'importation peut utiliser le mot-clé qualifié pour préfixer les noms par le nom du module dans lequel ils sont. Ces préfixes sont suivis par le caractère . sans espace intercalés. Les qualifieurs font partie de la syntaxe lexicale. Par conséquent, A.x et A . x sont totalement différents : le premier est un nom qualifié et le second est l'utilisation de la fonction infixe . Par exemple, en utilisant le module Tree introduit précédemment :
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
)))
Quelques programmeurs Haskell préfèrent utiliser les qualificateurs pour toutes les entités importées, en rendant explicite l'origine de chaque nom à chaque utilisation. D'autres préfèrent les noms courts et utilisent uniquement des qualificateurs lorsque cela s'avère 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 passerait-il si la même entité était importée depuis plus d'un module ? Heureusement, de tels conflits de noms sont autorisés : une entité peut être importée par différents chemins sans provoquer de conflit. Le compilateur sait si les entités de différents modules sont effectivement identiques.
XI-B. Les Types de données abstraits▲
En plus de définir des espaces de nommage, les modules fournissent la seule méthode de construire des types de données abstraits en Haskell. Par exemple, une caractéristique d'un ADT est que le type représenté est caché. Toutes les opérations sur cet ADT sont faites à un niveau abstrait qui ne dépend pas de cette représentation. Ainsi, bien que le type Tree soit assez simple pour que nous n'ayons pas besoin de recourir à une abstraction, un ADT adapté pourrait inclure les opérations suivantes :
data
Tree a – just the type
name
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 supportant cela peut être par exemple celui-ci :
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
Remarquez que dans la liste des exportations, le nom du type Tree apparaît seul (i.e. sans constructeurs). Par conséquent Leaf et Branch ne seront pas exportés, et le seul moyen de construire ou d'extraire une partie d'un arbre de ce module est d'utiliser différentes opérations abstraites. Bien entendu, le bénéfice qu'on tire de cacher ces informations est que, plus tard, nous pouvons changer le type dans lequel est stockée la donnée sans affecter les utilisateurs de ce type.
XI-C. Plus de caractéristiques▲
Vous trouverez ici un bref aperçu de quelques autres caractéristiques du système de modules. Vous pouvez vous reporter au rapport pour plus de détails…
- Une déclaration d'importation peut cacher des entités sur demande en utilisant la clause hiding dans la déclaration. Cela peut s'avérer utile pour exclure explicitement des noms qui sont utilisés pour d'autres objectifs sans avoir à utiliser des qualificateurs pour les noms importés depuis d'autres modules.
- Une importation peut contenir une clause as pour spécifier un qualificateur différent que le nom du module importé. Cela permet de raccourcir les noms de modules très longs ou d'adapter facilement le code lors d'un changement de module sans avoir à modifier tous les qualificateurs.
- Les programmes importent implicitement le module Prelude. Une importation explicite de ce module écraserait les noms importés lors de l'importation implicite. Par conséquent, le code ci-dessous n'importera pas length depuis le Prélude standard, ce qui permettra de définir ce nom différemment.
import
Prelude hiding
length
- Les déclarations d'instances ne sont pas explicitement nommées dans les listes d'importation ou d'exportation. Chaque module exporte toutes ses déclarations d'instances et chaque importation apporte toutes les déclarations d'instance dans sa portée.
- Les méthodes des classes peuvent être nommées soit de la manière des constructeurs de données, dans les parenthèses suivant le nom de la classe, soit comme des variables ordinaires.
Bien que le système de modules de Haskell soit relativement conservateur, il y a de nombreuses règles concernant l'importation et l'exportation des valeurs. La majorité d'entre elles sont évidentes. Par exemple, il est interdit d'importer deux entités différentes ayant le même nom dans la même portée. D'autres règles le sont moins. Par exemple, pour un type et une classe donnés, il ne peut pas y avoir plus d'une déclaration d'instance pour cette combinaison (type,classe) ailleurs dans le programme.
Le lecteur devrait lire le rapport en détail (section 5).