Les modules paramétrés ou foncteurs (en anglais functors) sont aux modules ce que les fonctions sont aux valeurs. Il s'agit de modules prenant en paramètre un module et renvoyant un autre module. Le module retourné peut à son tour être un foncteur, tout comme une fonction "à plusieurs arguments" est une fonction prenant un paramètre et retournant une autre fonction.
Les foncteurs permettent d'écrire des modules s'abstrayant des structures de données manipulées, dont la représentation et les fonctions de manipulations sont fournies par un module en paramètre. Il est alors aisément possible d'appliquer un même algorithme à deux structures de données différentes, tant que le module en paramètre fournit les fonctions pour effectuer les opérations de base.
La syntaxe de définition d'un foncteur est la même que pour un module, avec en plus le paramètre attendu:
Comme pour les fonctions, une syntaxe plus légère est possible:
L'expression de module peut également être un foncteur. La syntaxe suivante permet donc de simuler un foncteur à plusieurs arguments:
De la même façon, la déclaration d'un foncteur dans les interfaces et les signatures suit celle de déclaration de module:
L'application d'un foncteur se fait en utilisant la syntaxe suivante:
L'application d'un foncteur renvoyant un module, nous pouvons donc définir des modules par application de foncteurs de la façon suivante. Ici nous définissons un module Ord, contenant un type t et une fonction de comparaison compare. Nous passons ce module au foncteur Set.Make, qui permet d'obtenir un module de manipulation d'ensembles d'éléments du type t du module passé en paramètre, en l'occurrence Ord.t, donc ici des ensembles d'entiers.
# module Ord = struct type t = int let compare (x:int) (y:int) = Stdlib.compare x y end;; module Ord : sig type t = int val compare : int -> int -> int end # module Int_set = Set.Make(Ord);; module Int_set : sig type elt = Ord.t type t = Set.Make(Ord).t val empty : t val is_empty : t -> bool val mem : elt -> t -> bool val add : elt -> t -> t val singleton : elt -> t val remove : elt -> t -> t val union : t -> t -> t val inter : t -> t -> t val disjoint : t -> t -> bool val diff : t -> t -> t val compare : t -> t -> int val equal : t -> t -> bool val subset : t -> t -> bool val iter : (elt -> unit) -> t -> unit val map : (elt -> elt) -> t -> t val fold : (elt -> 'a -> 'a) -> t -> 'a -> 'a val for_all : (elt -> bool) -> t -> bool val exists : (elt -> bool) -> t -> bool val filter : (elt -> bool) -> t -> t val filter_map : (elt -> elt option) -> t -> t val partition : (elt -> bool) -> t -> t * t val cardinal : t -> int val elements : t -> elt list val min_elt : t -> elt val min_elt_opt : t -> elt option val max_elt : t -> elt val max_elt_opt : t -> elt option val choose : t -> elt val choose_opt : t -> elt option val split : elt -> t -> t * bool * t val find : elt -> t -> elt val find_opt : elt -> t -> elt option val find_first : (elt -> bool) -> t -> elt val find_first_opt : (elt -> bool) -> t -> elt option val find_last : (elt -> bool) -> t -> elt val find_last_opt : (elt -> bool) -> t -> elt option val of_list : elt list -> t val to_seq_from : elt -> t -> elt Seq.t val to_seq : t -> elt Seq.t val to_rev_seq : t -> elt Seq.t val add_seq : elt Seq.t -> t -> t val of_seq : elt Seq.t -> t end # let set = Int_set.of_list [ 1 ; 2 ; 2 ; 2 ; 3 ; 10 ];; val set : Int_set.t = <abstr> # Int_set.elements set;; - : Int_set.elt list = [1; 2; 3; 10]
Voyons la définition et l'utilisation de foncteurs au travers d'un exemple. Nous souhaitons modéliser un guichet auquel est associé une file d'attente. La file d'attente sera abstraite, permettant la mise en place de différentes politiques de priorité, de façon transparente pour le code de gestion du guichet.
Nous commençons donc par définir le type de la file d'attente:
# module type QueueType = sig type 'a t exception Empty val create : unit -> 'a t val pop : 'a t -> 'a val push : 'a -> 'a t -> unit end;; module type QueueType = sig type 'a t exception Empty val create : unit -> 'a t val pop : 'a t -> 'a val push : 'a -> 'a t -> unit end
Ensuite, nous définissons notre module de guichet, prenant en paramètre une file d'attente:
# module Guichet = functor (Q : QueueType) -> struct let create = Q.create let add = Q.push let handle_one f guichet = try f (Q.pop guichet) with Q.Empty -> () let rec handle_all f guichet = match try Some (Q.pop guichet) with Q.Empty -> None with | None -> () | Some doc -> f doc; handle_all f guichet end;; module Guichet : functor (Q : QueueType) -> sig val create : unit -> 'a Q.t val add : 'a -> 'a Q.t -> unit val handle_one : ('a -> unit) -> 'a Q.t -> unit val handle_all : ('a -> 'b) -> 'a Q.t -> unit end
Nous ne pouvons pas encore utiliser notre module, puisqu'il s'agit d'un foncteur:
# Guichet.create();; File "_none_", line 1, characters 0-14: Error: The module Guichet is a functor, it cannot have any components
Nous allons simuler notre guichet en utilisant d'abord une file FIFO. La signature demandée pour le module en paramètre est un sous-ensemble de la signature du module Queue, nous pouvons donc utiliser ce module comme modélisation de la file d'attente. En appliquant ce module à notre foncteur, nous obtenons un guichet "premier arrivé, premier servi":
# module Guichet_FIFO = Guichet(Queue);; module Guichet_FIFO : sig val create : unit -> 'a Queue.t val add : 'a -> 'a Queue.t -> unit val handle_one : ('a -> unit) -> 'a Queue.t -> unit val handle_all : ('a -> 'b) -> 'a Queue.t -> unit end # let fifo = Guichet_FIFO.create ();; val fifo : '_weak1 Queue.t = <abstr> # List.iter (fun n -> Guichet_FIFO.add n fifo) [ 1 ; 2 ; 3 ; 4 ; 5 ];; - : unit = () # Guichet_FIFO.handle_all (fun n -> print_int n; print_newline ()) fifo;; 1 2 3 4 5 - : unit = ()
Nous pouvons également modéliser la file d'attente par une pile; dans ce cas, le premier servi est le dernier arrivé:
# module Guichet_pile = Guichet(Stack);; module Guichet_pile : sig val create : unit -> 'a Stack.t val add : 'a -> 'a Stack.t -> unit val handle_one : ('a -> unit) -> 'a Stack.t -> unit val handle_all : ('a -> 'b) -> 'a Stack.t -> unit end # let pile = Guichet_pile.create ();; val pile : '_weak2 Stack.t = <abstr> # List.iter (fun n -> Guichet_pile.add n pile) [ 1 ; 2 ; 3 ; 4 ; 5 ];; - : unit = () # Guichet_pile.handle_all (fun n -> print_int n; print_newline ()) pile;; 5 4 3 2 1 - : unit = ()
Nous pouvons même faire un foncteur permettant de construire et tester un module de guichet, car les deux codes de test ci-dessus sont les mêmes:
# module Test (Q : QueueType) = struct module G = Guichet(Q) let guichet = G.create () let _ = List.iter (fun n -> G.add n guichet) [ 1 ; 2 ; 3 ; 4 ; 5 ] let _ = G.handle_all (fun n -> print_int n; print_newline ()) guichet end;; module Test : functor (Q : QueueType) -> sig module G : sig val create : unit -> 'a Q.t val add : 'a -> 'a Q.t -> unit val handle_one : ('a -> unit) -> 'a Q.t -> unit val handle_all : ('a -> 'b) -> 'a Q.t -> unit end val guichet : int Q.t end # module Foo = Test(Queue);; 1 2 3 4 5 module Foo : sig module G : sig val create : unit -> 'a Queue.t val add : 'a -> 'a Queue.t -> unit val handle_one : ('a -> unit) -> 'a Queue.t -> unit val handle_all : ('a -> 'b) -> 'a Queue.t -> unit end val guichet : int Queue.t end # module Foo = Test(Stack);; 5 4 3 2 1 module Foo : sig module G : sig val create : unit -> 'a Stack.t val add : 'a -> 'a Stack.t -> unit val handle_one : ('a -> unit) -> 'a Stack.t -> unit val handle_all : ('a -> 'b) -> 'a Stack.t -> unit end val guichet : int Stack.t end
On remarque que le compilateur impose correctement que les fonctions passées en paramètres aux fonctions handle_one et handle_all prennent en paramètres des valeurs du type des valeurs de la file d'attente:
# Guichet_FIFO.handle_one print_string fifo;; File "_none_", line 1, characters 37-41: Error: This expression has type int Queue.t but an expression was expected of type string Queue.t Type int is not compatible with type string
On aurait pu masquer la représentation interne du guichet en utilisant une signature et en ajoutant un type de guichet:
# module type GuichetAbstType = sig type 'a t val create : unit -> 'a t val add : 'a -> 'a t -> unit val handle_one : ('a -> unit) -> 'a t -> unit val handle_all : ('a -> 'b) -> 'a t -> unit end;; module type GuichetAbstType = sig type 'a t val create : unit -> 'a t val add : 'a -> 'a t -> unit val handle_one : ('a -> unit) -> 'a t -> unit val handle_all : ('a -> 'b) -> 'a t -> unit end
# module GuichetAbst (Q : QueueType) : GuichetAbstType = struct type 'a t = 'a Q.t include Guichet(Q) end;; module GuichetAbst : functor (Q : QueueType) -> GuichetAbstType # module Foo=GuichetAbst(Stack);; module Foo : sig type 'a t = 'a GuichetAbst(Stack).t val create : unit -> 'a t val add : 'a -> 'a t -> unit val handle_one : ('a -> unit) -> 'a t -> unit val handle_all : ('a -> 'b) -> 'a t -> unit end
On ne peut plus maintenant accéder à la représentation de la file depuis l'extérieur de la modélisation du guichet:
Il est possible de construire des modules anonymes (sans nom). C'est souvent le cas lors de l'application d'un foncteur prenant un petit module en paramètre. Dans ce cas, on utilise directement la syntaxe struct ... end plutôt que la syntaxe module M = ... et l'utilisation de M dans la suite:
Les foncteurs en OCaml sont dits applicatifs, c'est-à-dire qu'appliquer deux fois le même foncteur au même paramètre retournera deux modules dont les types sont identiques. Dans l'exemple ci-dessous, nous créons deux modules M1 et M2 en appliquant le même foncteur au même module en paramètre. Bien que la signature du module résultat indique que le type t est abstrait, M1.t et M2.t sont identiques: nous pouvons utiliser M2.compare pour comparer les deux valeurs créées avec M1.create().
# module type I = sig type t val create : unit -> t val compare : t -> t -> int end;; module type I = sig type t val create : unit -> t val compare : t -> t -> int end # module F (P: I) : I = struct type t = P.t let create = P.create let compare = P.compare end;; module F : functor (P : I) -> I # module Int : I = struct type t = int let create () = 12 let compare = Stdlib.compare end;; module Int : I # module M1 = F(Int);; module M1 : sig type t = F(Int).t val create : unit -> t val compare : t -> t -> int end # module M2 = F(Int);; module M2 : sig type t = F(Int).t val create : unit -> t val compare : t -> t -> int end # let v1 = M1.create();; val v1 : M1.t = <abstr> # let v2 = M1.create();; val v2 : M1.t = <abstr> # M2.compare v1 v2 ;; - : int = 0
Il est possible de créer des foncteurs dits génératifs, c'est-à-dire qui créent de nouveaux types lorsqu'ils sont appliqués. Ces foncteurs prennent un argument () en paramètre. En reprenant l'exemple ci-dessus, nous créons un nouveau foncteur F2 prenant aussi () en paramètre.
# module F2 (P:I) () : I = struct type t = P.t let create = P.create let compare = P.compare end;; module F2 : functor (P : I) () -> I # module M1 = F2(Int) ();; module M1 : I # module M2 = F2(Int) ();; module M2 : I
Les nouveaux modules M1 et M2, construits en appliquant F2 à Int et () ont maintenant des types t incompatibles:
# let v1 = M1.create();; val v1 : M1.t = <abstr> # let v2 = M1.create();; val v2 : M1.t = <abstr> # M2.compare v1 v2 ;; File "_none_", line 1, characters 11-13: Error: This expression has type M1.t but an expression was expected of type M2.t
Ce type de foncteur est utile notamment lorsqu'on souhaite utiliser une même structure de données à base d'indices ou de clés mais qu'on veut que le typage nous garantisse que l'on n'utilise pas une clé d'une structure de données pour une autre structure.