• [^] # Re: Module et type abstrait

    Posté par . En réponse au journal Une petite histoire d'utilisation type fort dans Ocaml. Évalué à 2.

    Il est vrai que la notation pour l'application des constructeurs de types inductifs peut apparaître contre intuitive, car elle n'est pas consistante avec celle des fonctions : c'est parce que en OCaml, on ne peut pas faire d'application partielle sur un constructeur de type (contrairement à Haskell, il me semble). Résultat les paramètres se passent entre parenthèses, comme en python par exemple.

    Pour la taille mémoire, 5 ou 3 mots, cela se voit ainsi : un type somme occupe 1 mot mémoire qui contient des informations sur entre autre la structure des paramètres, plus 1 mot par paramètre. Ainsi dans le cas Coord of (int * int), on a 1 mot + 1 mot pour le paramètre qui pointe sur une paire int * int, paramètre qui contient lui même 1 mot de « tag » et 1 mot pour chaque int; d'où un total de 5 mots avec une indirection. Dans le cas Coord of int * int, on a 1 mot de « tag » plus 1 mot par paramètre, soit un total de 3 mots.

    Si tu veux pouvoir créer une variable de type Coord of int * int à partir d'une paire préexistante, il faut passer par un «smart » constructeur, c'est-à-dire une fonction de qui prend une paire et renvoie une valeur du bon type (à l'instar de mes fonctions make dans mes exemples avec modules).

    type coord = Coord of int * int;;
    type coord = Coord of int * int
    let coord (x,y) = Coord (x,y);;
    val coord : int * int -> coord = <fun>

    Sinon pour revenir sur l'approche modulaire, un de ses intérêts pourrait être, dans un premier temps, de te familiariser avec le système de module. Ensuite, tu pourrais jeter un œil sur les fonctions qui prennent des modules comme arguments pour retourner des modules, ce que l'on appelle des foncteurs. Comme ton projet consiste à afficher des schémas kicad, je vais comparer cela à l'électronique. Quand tu fais un schéma de circuit, certains composants peuvent être remplacés par d'autres tant qu'ils fonctionnement de la même façon : c'est cela un module, un composant. Et ton foncteur, c'est le schéma de ton circuit qui décrit comment les assembler pour fabriquer un nouveau composant. Si dedans tu as un micro contrôleur, tu peux opter lors de la réalisation entre des modèles de différents fabricants (qui peuvent différer sur leur structure interne) tant qu'ils réalisent la même fonction et exposent la même interface.

    Si je reprends mon interface de coordonnées en lui ajoutant quelques fonctions :

    module type Coord = sig
     type t
     val make : int -> int -> t 
     val to_pair : t -> int * int
     val move : ?dx:int -> ?dy:int -> t -> t
     val compare : t -> t -> int
    end

    On peut l'implémenter avec des couples ou des paires (enregistrements) :

    module PCoord : Coord = struct
     type t = int * int
     let make i j = (i, j)
     let to_pair p = p
     let move ?(dx = 0) ?(dy = 0) (x,y) = (x + dx, y + dy)
     let compare (i, j) (i', j') =
     let c = compare i i' in
     if c = 0 then compare j j' else c
    end
    module RCoord : Coord = struct
     type t = {x:int; y:int}
     let make i j = {x = i; y = j}
     let to_pair {x; y} = (x,y)
     let move ?(dx = 0) ?(dy = 0) p = {x = p.x + dx ; y = p.y + dy}
     let compare p p' =
     let c = compare p.x p'.x in
     if c = 0 then compare p.y p'.y else c
    end;;

    Comme ils définissent un type t et une fonction de comparaison compare, tu peux obtenir directement un module pour gérer des ensembles de coordonnées, en utilisant le foncteur du module Set de la bibliothèque standard :

    module F = Set.Make(PCoord);;
    module F :
     sig
     type elt = PCoord.t 
     type t = Set.Make(PCoord).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 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 partition : (elt -> bool) -> t -> t * t
     val cardinal : t -> int
     val elements : t -> elt list
     val min_elt : t -> elt
     val max_elt : t -> elt
     val choose : t -> elt
     val split : elt -> t -> t * bool * t
     val find : elt -> t -> elt
     val of_list : elt list -> t
     end

    Tu peux voir ça comme si ton module offrait plusieurs pins en interface et que tu routes certains d'entre eux (le type t et la fonction compare) vers une partie de ton circuit pour avoir des ensembles. Mais dans la pratique, tu peux choisir un modèle de composant plutôt qu'un autre pour des raisons de performance (bien que dans mon exemple bateau, les deux doivent être sensiblement identiques une fois compilés).

    Sapere aude ! Aie le courage de te servir de ton propre entendement. Voilà la devise des Lumières.