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 Show
instances:
{-# 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:
- La seule opération utile que vous pouvez effectuer sur un
AnyShape
est d'obtenir sa zone.
- Vous devez toujours utiliser le
AnyShape
constructeur pour importer l'un des types de forme dans le AnyShape
type.
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 Shape
type , et vous écrivez des fonctions pour convertir vos cercles et carrés en Shape
s.
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 AnyShape
types d'en haut Double
sont 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 ST
type 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.