Les interfaces de Java permettent à un objet de prendre en argument un autre objet, de n'importe quelle classe, du moment que l'objet en argument possède certaines méthodes. C'est donc une forme de polymorphisme, les arguments peuvent avoir un type quelconque, mais avec la possibilité de mettre des contraintes additionnelles sur le type.
En OCaml, nous avons vu que les variables polymorphes peuvent avoir tous les types. Nous n'avons pas de façon de contraindre une variable à avoir n'importe quel type mais avec une fonction associée. En un sens, nous n'en avons pas vraiment besoin : si la fonction prenant cette variable en argument a besoin de faire une opération propre à cet argument $x$, c'est-à-dire qu'elle doit appeler une fonction $f$propre au type de $x$, il lui suffit de prendre aussi $f$ en argument.
Prenons l'exemple des files de priorités. Les files de priorités sont définies génériquement sur le type des éléments contenus dans les files. Mais une contrainte est de pouvoir comparer les éléments, puisque la raison d'être des files de priorité est de récupérer les éléments dans un ordre particulier. En Java, on pourrait donc définir une classe générique et imposer que le type des clés étendent la classe Comparable.
Il n'y a pas de mécanisme équivalent en OCaml. Au passage, la solution Java est loin d'être parfaite, et d'ailleurs la librairie standard de Java ne fait pas comme cela. La raison est qu'avec cette solution, pour tout classe peut exister qu'une seule sorte de file de priorité : l'ordre est fixé par la classe. On veut plutôt séparer la classe des clés de l'ordre sur les clés: ce sont deux choses distinctes. Comme ci-dessus, il est donc préférable de donner la fonction de comparaison (un objet d'interface Comparator) au constructeur de la file de priorité : on en revient donc à la solution fonctionnelle.
Néanmoins, OCaml propose un mécanisme alternatif, les foncteurs (à ne pas confondre avec l'interface foncteur). Le principe est de générer des définitions en fonction des valeurs définies par un module. Par exemple, définir un module de file de priorité en fonction d'un module d'interface Set.OrderedType.
Un foncteur est une sorte de fonction prenant en argument un module, et ayant pour résultat un module. Par exemple, pour créer un dictionnaire dont les clés sont les chaînes de caractères, on peut écrire :
Ici, le module Make du module Map est un foncteur, et il est appliqué au module String, son argument. Le résultat est le module StringMap. Regardons le type de Map.Make :
Map.S est l'interface des dictionnaires. On retrouve la flèche -> des fonctions, précédée de l'argument et suivi du résultat. Techniquement les foncteurs ne sont pas des fonctions : les fonctions vivent au niveau des valeurs, alors que les foncteurs vivent uniquement au niveau des modules. On ne peut pas produire un module à partir d'une valeur, ou une valeur à partir d'un module (en tout cas, pas comme cela).
L'argument K est annoté par le type de module Set.OrderedType. Cela signifie que pour appliquer Map.Make à un module, il faut que ce dernier implémente l'interface Set.OrderedType : avoir un type t et une fonction compare du bon type. On peut donc en déduire comment réaliser un dictionnaire dont les clés sont les entiers :
Il n'est par contre pas possible de faire un dictionnaire sur les listes contenant des éléments de type arbitraire, puisque le type t de OrderedType doit ne pas avoir de paramêtre. On peut néanmoins faire des dictionnaires de listes d'entiers :
La librairie standard contient des foncteurs pour créer des structures d'ensembles (Set.Make), de dictionnaires (Map.Make) et de tables de hachage (Hashtbl.Make).
En dehors des structures de données, les principaux cas d'utilisation des foncteurs sont en général soit pour étendre automatiquement un module implémentant une interface minimaliste en lui ajoutant des fonctions supplémentaires, soit pour créer des niveaux d'abstraction dans une implémentation. Un exemple classique est la librairie Cohttp qui permet l'implémentation haut-niveau de clients et de serveurs http. Cohttp fonctionne sur plusieurs architectures, et il est facile d'en ajouter de nouvelles. Pourquoi ? les couches haut-niveaux de cohttp sont générés par des foncteurs, prenant en argument une implémentation des couches bas-niveaux (du module IO spécifiquement). Une autre application des foncteurs est le paramétrage d'un programme : cela permet de pouvoir ensuite facilement changer les paramètres, et même de tester différents paramêtrages avec un seul programme.
Pour créer un foncteur, il est préférable de commencer par créer une interface pour l'argument et une autre pour le résultat (si elles n'existent pas). Ce n'est pas strictement nécessaire, mais il sera obligatoire d'annoter le type du module en argument, donc autant le nommer. Nommer l'interface résultat est aussi une bonne pratique pour rendre le résultat lisible.
Prenons l'exemple d'un module de file de priorité basée sur des tas gauches. Le foncteur transformera un type ordonnée en un type des files de priorités. On définit :
Make est un nom classique pour un foncteur, comme toujours il apparait dans un autre module, par exemple son nom complet ici sera probablement Heap.Make, ce qui est parfaitement clair. On peut néanmoins choisir un nom arbitraire selon la syntaxe des noms de modules, autrement dit commençant par une majuscule. La syntaxe pour définir le foncteur est alors :
L'annotation de type pour Make est optionnelle, elle est en revanche obligatoire pour l'argument Key du foncteur. La suite du code est simple : on remarque simplement que les valeurs définies dans le foncteur peuvent utilisées les valeurs définies dans l'argument du foncteur. Par exemple le type key est définie à l'aide du type Key.t, et la fonction de comparaison (<) est définie à l'aide de Key.compare.
Une des difficultés principales dans l'utilisation des foncteurs est de réussir à s'assurer que les types sont correctement exportés. Par exemple l'implémentation ci-dessus est inutilisable : en effet, en imposant l'interface MinimalHeap, nous avons rendu abstraits les types t (ce qui est normal) et key (ce qui est génant). L'application du foncteur fournira donc un type des files de priorité dont les éléments font partis d'un type inconnu !
Une mauvaise solution serait de retirer l'annotation de type pour le module produit MinimalHeap. Mais il n'est pas négociable de perdre les possibilités d'abstraction que procurent les interfaces. Dans le cas des foncteurs, la solution consiste en l'ajout d'export explicite des types que l'on souhaite exporter. La syntaxe est alors la suivante :
La syntaxe with type permet de rendre visible le fait que le type key défini par le foncteur est le même que le type Key.t de l'argument du foncteur. De même, il est possible de préciser des interfaces pour les modules définis par le foncteur : on aurait pu écrire :
Il est possible d'exporter plusieurs déclarations de types et de modules en les séparant par le mot-clé and :