• [^] # Re: Jai: Language for game programmming

    Posté par . En réponse à la dépêche C++17, Genèse d’une version mineure. Évalué à 5. Dernière modification le 14 octobre 2016 à 11:23.

    Pour les cas que tu décris en Haskell, c'est ce qui manque à l'heure actuelle en OCaml : le polymorphisme ad hoc (en), qu'il faut distinguer du polymorphisme paramétrique (en). Le second est le polymorphisme que l'on trouve en OCaml : une fonction peut prendre en entrée un type quelconque sans que cela change son code dans sa forme (l'algorithme est identique) comme c'est le cas avec des fonctions sur les 'a list ou 'a array, par exemple.

    Le polymorphisme ad hoc, lui, est ce qui permet entre autre de faire de la surcharge d'opérateur : une multiplicité de code partage le même alias, et le code adapté sera choisi en fonction du type des entrées. C'est par exemple pour cela qu'en Haskell il y a un seul opérateur d'addition + alors qu'en OCaml il y en un pour chaque type de données : + pour les int et +. pour les float. Ce sont les type classes qui permettent cela en Haskell, dispositif qui ne possède pas encore d'équivalent en OCaml mais il y a des personnes qui y travaillent.

    Leo White, Frédéric Bour et Jeremy Yallop ont proposé les modular implicts qui s'insprirent entre autre des type classes de Haskell et des implicits de Scala. Dans leur article, ils comparent leur approche avec celles faites dans d'autres langages, comme les concepts pour le C++, section qui renvoie à un article collaboratif dont un des auteurs est Bjarne Stroustrup : Concepts: linguistic support for generic programming in C++. Comme je ne suis pas abonné à l'ACM, je n'ai pas pu lire l'article, et je me demande si ce sont bien les concept que les développeurs C++ attendent et dont parle la dépêche.

    L'idée de base pour ajouter cette possibilité dans le langage OCaml est de passer par le système des modules du langage et des fonctions sur les modules ou foncteurs. Une approches lourde et qui crée beaucoup d'indirection à base de pointeurs, dans l'exemple de l'addition, est de passer par un type somme :

    type num = I of int | F of float
    let add i j = match i, j with
     |I i, I j -> I (i + j)
     |I i, F j -> F (float_of_int i +. j)
     |F i, I j -> F (i +. float_of_int j)
     |F i, F j -> F (i +. j)
    add (I 1) (F 2.3);;
    - : num = F 3.3

    C'est extrêmement lourd dans l'écriture à l'usage. Leur idée est donc de passer par le système de modules et les modules de première classe. On commence par définir une signature pour les objets numériques :

    module type Num = sig
     type t
     val add: t -> t -> t
     val mul: t -> t -> t
    end

    Puis, on définit deux fonctions1 add et mul qui prennent, en plus de leurs deux paramètres habituels, un module qui choisira l'implémentation adaptée.

    let add (type a) (module M:Num with type t = a) (i:a) (j:a) = M.add i j;;
    val add : (module Num with type t = 'a) -> 'a -> 'a -> 'a = <fun>
    let mul (type a) (module M:Num with type t = a) (i:a) (j:a) = M.mul i j;;
    val mul : (module Num with type t = 'a) -> 'a -> 'a -> 'a = <fun>

    Il reste à implémenter deux modules de la bonne signature pour nos deux types de base int et float.

    module Int_num : (Num with type t = int) = struct
     type t = int
     let add = (+)
     let mul = ( * )
    end;;
    module Int_num :
     sig type t = int val add : t -> t -> t val mul : t -> t -> t end
    module Float_num : (Num with type t = float) = struct
     type t = float
     let add = (+.)
     let mul = ( *. )
    end;;
    module Float_num :
     sig type t = int val add : t -> t -> t val mul : t -> t -> t end

    Pour l'usage cela se passe ainsi :

    add (module Int_num) 1 2;;
    - : int = 3
    add (module Float_num) 1.3 2.5;;
    - : float = 3.8

    Leur proposition consiste à ajouter quelques modifications légères au langage de base de telle façon que le module en argument soit implicite et inféré automatiquement par le compilateur en fonction du type des autres paramètres. Ce qui permettrait d'avoir la syntaxe (qui n'est pas acceptée à l'heure actuelle) suivante :

    (* ajout du mot clé `implicit` en tête de la définition du module *)
    implicit module Int_num : Num = struct
     type t = int
     let add = (+)
     let mul = ( * )
    end
    implicit module Float_num : Num = struct
     type t = float
     let add = (+.)
     let mul = ( *. )
    end
    (* le paramètre implicite est mis entre accolades {}
     * et non entre parenthèse () *)
    let add {M : Num} (i : M.t) (j : M.t) = M.add i j
    add 1 2;;
    - : int = 3
    add 1.3 2.5;;
    - : float = 3.8

    Le compilateur se chargeant de regarder, dans le dictionnaire des modules implicites ayant la bonne signature, de trouver celui dont le type t correspond à celui des paramètres donnés à la fonction.


    1. en réalité ce n'est pas tout à fait ce choix qui est fait pour l'implémententation des fonctions add et mul, mais c'est pour simplifier, et ils justifient leur approche à la section 2.4 qui permet d'atteindre un plus haut niveau de polymorphisme.

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