• [^] # Re: l'héritage et les exemples pourris

    Posté par . En réponse au lien « Clean code » : performances lamentables. Évalué à 4.

    Je le trouve inadapté pour modéliser des concepts issus des langages fonctionnels

    Ça ne sert effectivement à rien. Un jour j'ai demandé à quelqu'un de m'expliquer l'UML, au bout de dix minutes je lui ai dit d'arrêter : je n'avais rien à apprendre d'une personne qui ne sait pas structurer logiquement sa pensée de manière adéquate. En revanche j'use abondamment de diagrammes : ceux de la théorie des catégories. Bon après, j'ai une gros rejet de la POO, non du concept d'objet de la POO, mais du paradigme dans lequel tout est objet (à la Java). C'est pour moi un paradigme d'asile d'aliéné, ou l'art de faire de l'algèbre en marchant sur la tête. Je m'explique en reprenant le cas d'étude de l'article.

    Qu'un carré, un rectangle ou un cercle soient des formes, il n'y a là aucun doute. Mais cette propriété n'a rien à foutre dans leur définition ! Ce qui est inévitablement le cas si on les définit comme des objets. Personnellement je les définit ainsi :

    module Carré = struct
     type t = {width : float}
     let make width = {width}
     let area {width} = width *. width
    end
    module Rectangle = struct
     type t = {
     width : float;
     length : float;
     }
     let make width length = {widht; length}
     let area {width; length} = width *. length
    end
    module Cercle = struct
     type t = {radius : float}
     let make radius = {radius}
     let area {radius} = Float.pi *. radius *. radius
    end

    Ce qui définit chacun des types, ce sont les enregistrements et la propriété qu'ils ont d'avoir une surface se traduit par l'existence d'une fonction area sur chacun de ces types. Après le concept d'un objet, au sens de la POO, c'est le type suprême de toutes les formes, celui dans lequel on peut injecter n'importe quel forme. Ce que ne semble pas avoir compris l'auteur de l'article du lien, puisqu'il ne considère que le type somme de quatre formes possibles, alors qu'il y a aussi n'importe quel polygone, des ellipses...

    Et voilà comment on définit, algébriquement, un tel type :

    module type Shape = sig
     module type S = sig
     type t
     val area : t -> float
     end
     type 'a meth = (module S with type t = 'a)
     (* les valeurs de type `t` sont des formes *)
     type t
     (* toute valeur d'un type `'a` qui possède une méthode `area` est une forme *)
     val make : 'a meth -> 'a -> t
     val area : t -> float
    end

    Je n'ai donné que l'interface (ou API) d'un module qui définit ce genre suprême, sans préciser les détails d'implémentation ("Code should not know about the internals of objects it’s working with" ;-). Pour l'implémentation du type Shape.t je peux prendre aux choix :

    • un objet : type t = < area : unit -> float >
    • une clôture : type t = unit -> float
    • un gadt : type t = Shape : {meth : 'a meth; self : 'a} -> t

    et la liste n'est pas exhaustive.

    Et c'est là qu'interviennent les diagrammes de la théorie des catégories. Une forme est caractérisée par l'existence d'une fonction area sur son type. Algébriquement cela se traduit par l'étude des structures algébriques ayant la signature Shape.S :

    module type S =
     type t
     val area : t -> float
    end

    On peut faire un graphe orienté de toutes ces structures : chaque nœud du graphe est un module satisfaisant cette signature, et chaque arête est une fonction qui transforme un nœud en un autre (par exemple, la proposition tous les carrés sont des rectangles est représentée par une telle arrête). Mais dans ce graphe (qui, en soit, contient une infinité de nœuds), il y a un nœud particulier T (ou plutôt une famille de nœud). Ce dernier à la propriété que pour tout autre nœud N du graphe il existe une et une seule arrête qui va de N à T. Ce nœud T est le type Top de toutes ces structures algébriques, celui qui les contient toutes (les carrés, les rectangles, les cercles...). En réalité, il y a une infinité de tels nœuds Top, mais ils sont tous équivalents et ne diffèrent que par leurs détails d'implémentation (j'en ai donné 3 possibles au-dessus). La fonction centrale de cette structure c'est de faire du dynamic dispacth ou du polymorphisme ad-hoc : il applique la fonction area adéquate à la forme qu'il contient. Mais la stratégie à base d'objets et de vtable n'est qu'une solution parmi d'autre pour faire cela. On trouve par exemple, dans des langages existant :

    • les modules (ou les objets) en OCaml ;
    • les objets dans tout langage orienté POO ;
    • les interface en Golang ;
    • les type class en Haskell ;
    • les template en C++ (c'est l'équivalent des modules OCaml) ;
    • les traits en Rust ;
    • et bien d'autres encore.

    Mais pour définir une fonction sur une liste de formes, ou sur une forme quelconque, c'est-à-dire pour faire du polymorphisme ad-hoc, il n'est absolument pas nécessaire de les injecter dans le type Top (qui est le type des objets de la POO), et c'est ce que je reproche à la POO : elle réduit la catégorie à son seul objet terminal (ce type Top), ce qui est une manière plus que bancal de faire de l'algèbre. D'autant que de nombreuses catégories de structures algébriques n'ont pas d'objet terminal, à commencer par toutes celles qui ont des opérateurs binaires (addition, multiplication), où il devient alors vraiment ridicule de suivre le paradigme du tout objet.

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