Pourquoi (ou pourquoi pas) les types existentiels sont-ils considérés comme de mauvaises pratiques en programmation fonctionnelle?

43

Quelles sont les techniques que je pourrais utiliser pour refactoriser de manière cohérente le code en supprimant la dépendance aux types existentiels? Celles-ci sont généralement utilisées pour disqualifier les constructions non désirées de votre type, ainsi que pour permettre une consommation avec un minimum de connaissances sur le type donné (ou c'est ce que je comprends).

Quelqu'un a-t-il mis au point un moyen simple et cohérent de supprimer l'utilisation de ces codes dans le code tout en conservant certains des avantages? Ou au moins un moyen de glisser dans une abstraction qui permette de les supprimer sans nécessiter un manque de code important pour faire face à la modification?

Vous pouvez en savoir plus sur les types existentiels ici ("si vous osez ..").

Petr Pudlák
la source
8
@RobertHarvey J'ai trouvé l'article du blog qui m'a fait réfléchir à cette question pour la première fois: Haskell Antipattern: Existential Typeclass . En outre, l'article que Joey Adams mentionne décrit certains problèmes de nature existentielle dans la section 3.1. Si vous avez des arguments contraires, merci de les partager.
Petr Pudlák
5
@ PetrPudlák: Gardez à l'esprit qu'il n'y a pas de type existentiel en général, mais une utilisation particulière de ceux-ci lorsque quelque chose de plus simple et plus simple (et mieux pris en charge en Haskell) ferait le même travail.
CA McCann
10
Si vous voulez savoir pourquoi l'auteur d'un article de blog particulier a exprimé une opinion, la personne à laquelle vous devriez probablement demander est l'auteur.
Eric Lippert
6
@Ptolemy C'est une question d'opinion. En utilisant Haskell depuis des années, je ne peux pas difficilement imaginer utiliser un langage fonctionnel qui n’a pas de système de type fort.
Petr Pudlák
4
@Ptolemy: Le mantra de Haskell est "s'il compile ça marche", le mantra d'Erlangs est "laissez-le s'écraser". La frappe dynamique est bonne comme colle, mais personnellement, je ne construirais pas des choses en utilisant simplement la colle.
Den

Réponses:

8

Les types existentiels ne sont pas vraiment considérés comme une mauvaise pratique en programmation fonctionnelle. Je pense que ce qui vous inquiète , c’est que l’une des utilisations les plus couramment citées des existentiels est l’ antipattern de la classe de types existentielle , ce que beaucoup de gens pensent être une mauvaise pratique.

Cette tendance est souvent mise au jour en réponse à la question de savoir comment avoir une liste d’éléments de type hétérogène implémentant la même classe de types. Par exemple, vous voudrez peut-être avoir une liste de valeurs qui ont des Showinstances:

{-# LANGUAGE ExistentialTypes #-}

class Shape s where
   area :: s -> Double

newtype Circle = Circle { radius :: Double }
instance Shape Circle where
   area (Circle r) = pi * r^2

newtype Square = Square { side :: Double }
    area (Square s) = s^2

data AnyShape = forall x. Shape x => AnyShape x
instance Shape AnyShape where
    area (AnyShape x) = area x

example :: [AnyShape]
example = [AnyShape (Circle 1.0), AnyShape (Square 1.0)]

Le problème avec un code comme celui-ci est le suivant:

  1. La seule opération utile que vous pouvez effectuer sur un AnyShapeest d'obtenir sa zone.
  2. Vous devez toujours utiliser le AnyShapeconstructeur pour importer l'un des types de forme dans le AnyShapetype.

Donc, il s’avère que ce morceau de code ne vous procure pas vraiment quelque chose que ce plus court ne:

class Shape s where
   area :: s -> Double

newtype Circle = Circle { radius :: Double }
instance Shape Circle where
   area (Circle r) = pi * r^2

newtype Square = Square { side :: Double }
    area (Square s) = s^2

example :: [Double]
example = [area (Circle 1.0), area (Square 1.0)]

Dans le cas de classes multi-méthodes, le même effet peut généralement être obtenu plus simplement en utilisant un codage "enregistrement de méthodes" - au lieu d'utiliser une classe comme Shape, vous définissez un type d'enregistrement dont les champs sont les "méthodes" du Shapetype , et vous écrivez des fonctions pour convertir vos cercles et carrés en Shapes.


Mais cela ne signifie pas que les types existentiels sont un problème! Par exemple, dans Rust, ils ont une fonctionnalité appelée objets de trait que les gens décrivent souvent comme un type existentiel par rapport à un trait (les versions de Rust des classes de types). Si les classes de types existentielles sont un anti-modèle dans Haskell, cela signifie-t-il que Rust a choisi une mauvaise solution? Non! La motivation dans le monde Haskell repose sur la syntaxe et la commodité, pas vraiment sur le principe.

Une façon plus mathématique de formuler ceci est de souligner que les AnyShapetypes d'en haut Doublesont isomorphes - il existe une "conversion sans perte" entre eux (enfin, sauf pour la précision en virgule flottante):

forward :: AnyShape -> Double
forward = area

backward :: Double -> AnyShape
backward x = AnyShape (Square (sqrt x))

Donc, à proprement parler, vous ne gagnez ou ne perdez aucun pouvoir en choisissant l'un par rapport à l'autre. Ce qui signifie que le choix devrait être basé sur d'autres facteurs tels que la facilité d'utilisation ou les performances.


Et gardez à l'esprit que les types existentiels ont d'autres utilisations en dehors de cet exemple de listes hétérogènes, il est donc bon de les avoir. Par exemple, le STtype de Haskell , qui nous permet d'écrire des fonctions purement externes mais qui utilisent des opérations de mutation de mémoire en interne, utilise une technique basée sur des types existentiels afin de garantir la sécurité lors de la compilation.

Donc, la réponse générale est qu'il n'y a pas de réponse générale. Les utilisations de types existentiels ne peuvent être jugées que dans le contexte - et les réponses peuvent être différentes selon les caractéristiques et la syntaxe fournies par différentes langues.

sacundim
la source
Ce n'est pas un isomorphisme.
Ryan Reich
2

Je ne connais pas trop Haskell, je vais donc essayer de répondre à la partie générale de la question en tant que développeur C # fonctionnel et non académique.

Après avoir lu un peu, il s'avère que:

  1. Les caractères génériques Java sont similaires aux types existentiels:

    Différence entre les types existentiels de Scala et le caractère générique de Java par exemple

  2. Les caractères génériques ne sont pas implémentés complètement en C #: la variance générique est prise en charge, mais la variance de site d’appel n’est pas:

    Génériques C #: Caractères génériques

  3. Vous n’avez peut-être pas besoin de cette fonctionnalité tous les jours, mais quand vous le ferez, vous la sentirez (par exemple, avoir à introduire un type supplémentaire pour que les choses fonctionnent):

    Caractères génériques dans les contraintes génériques C #

Sur la base de ces informations, les types existentiels / caractères génériques sont utiles lorsqu'ils sont implémentés correctement et ne présentent aucun problème en eux-mêmes, mais ils peuvent probablement être utilisés de manière abusive, tout comme les autres fonctionnalités du langage.

Tanière
la source