web-dev-qa-db-fra.com

Pourquoi (ou pourquoi pas) sont des types existentiels considérés comme une mauvaise pratique dans la programmation fonctionnelle?

Quelles sont certaines techniques que je pourrais utiliser pour refroidir systématiquement le code de refacteur supprimant la dépendance des types existentiels? Celles-ci sont généralement utilisées pour disqualifier les constructions indésirables de votre type ainsi que pour permettre la consommation avec un minimum de connaissances sur le type donné (ou de même que je comprends).

Quelqu'un a-t-il d'une manière cohérente de manière cohérente de supprimer la dépendance à ces problèmes qui maintiennent toujours certains des avantages? Ou au moins toute façons de glisser dans une abstraction qui permet de supprimer sans nécessiter une caisse significative du changement de code pour faire face à l'altération?

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

43
Petr Pudlák

Les types existentiels ne sont pas vraiment considérés comme des mauvaises pratiques dans la programmation fonctionnelle. Je pense que ce qui vous échappe est que l'une des utilisations les plus couramment citées pour des existentielles est la Typeclass existentielle antipattern , que beaucoup de gens croient est une mauvaise pratique.

Ce modèle est souvent trotté comme une réponse à la question de savoir comment avoir une liste d'éléments typés d'hétérogènes qui mettent tous la même manière que la même typlass. Par exemple, vous voudrez peut-être avoir une liste de valeurs qui ont 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 le code comme celui-ci est le suivant:

  1. La seule opération utile que vous puissiez effectuer sur un AnyShape est d'obtenir sa zone.
  2. Vous devez toujours utiliser le constructeur AnyShape pour apporter l'un des types de forme dans le type AnyShape.

Donc, comme il s'avère, ce morceau de code ne vous fait pas vraiment que ce plus court que ce soit plus court:

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 être généralement accompli plus simplement en utilisant un codage "enregistrement des méthodes" au lieu d'utiliser une typlass comme Shape, vous définissez un type d'enregistrement dont les champs sont les champs. "Méthodes" du type Shape Type et vous écrivez des fonctions pour convertir vos cercles et vos carrés en Shapes.


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

Une manière plus mathématique de mettre cela souligne que le type AnyShape de dessus et Double sont isomorphe - là-bas est une "conversion sans perte" entre eux (Eh bien, sauvegarder pour la précision du point flottant):

forward :: AnyShape -> Double
forward = area

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

Si strictement parlant, vous ne gagnez ni ne perdez aucun pouvoir en choisissant un contre l'autre. Ce qui signifie que le choix doit être basé sur d'autres facteurs tels que la facilité d'utilisation ou la performance.


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

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

8
sacundim

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

Après avoir fait de la lecture, 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 la carte générique de Java par exemple

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

    C # Generics: Wildcards

  3. Vous n'avez peut-être pas besoin de cette fonctionnalité tous les jours, mais lorsque vous le sentirez, vous le sentirez (par exemple, avoir à introduire un type supplémentaire pour faire fonctionner les choses):

    Wildcards dans les contraintes génériques C #

Sur la base de ces informations, des types existentiels/Wildcards sont utiles lorsqu'ils sont mis en œuvre correctement et il n'y a rien de mal avec eux en soi, mais ils peuvent probablement être mal utilisés comme d'autres caractéristiques de la langue.

2
Den