Los tipos existenciales no se consideran realmente una mala práctica en la programación funcional. Creo que lo que te está tropezando es que uno de los usos más comúnmente citados para los existenciales es el antipatrón de clase de tipo existencial , que mucha gente cree que es una mala práctica.
Este patrón a menudo se presenta como una respuesta a la pregunta de cómo tener una lista de elementos de tipo heterogéneo que implementen la misma clase de tipos. Por ejemplo, es posible que desee tener una lista de valores que tengan Show
instancias:
{-# 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)]
El problema con un código como este es este:
- La única operación útil que puede realizar en un
AnyShape
es obtener su área.
- Aún necesita usar el
AnyShape
constructor para incorporar uno de los tipos de formas al AnyShape
tipo.
Resulta que ese código realmente no te da nada que este más corto no:
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)]
En el caso de las clases de métodos múltiples, el mismo efecto generalmente se puede lograr de manera más simple mediante el uso de una codificación de "registro de métodos"; en lugar de usar una clase de tipo como Shape
, usted define un tipo de registro cuyos campos son los "métodos" del Shape
tipo , y escribes funciones para convertir tus círculos y cuadrados en Shape
s.
¡Pero eso no significa que los tipos existenciales sean un problema! Por ejemplo, en Rust tienen una característica llamada objetos de rasgos que las personas a menudo describen como un tipo existencial sobre un rasgo (las versiones de Rust de las clases de tipos). Si las clases de tipos existenciales son un antipatrón en Haskell, ¿eso significa que Rust eligió una mala solución? ¡No! La motivación en el mundo de Haskell es sobre la sintaxis y la conveniencia, no sobre el principio.
Una forma más matemática de decir esto es señalar que el AnyShape
tipo de arriba y Double
son isomórficos : hay una "conversión sin pérdidas" entre ellos (bueno, salvo la precisión de coma flotante):
forward :: AnyShape -> Double
forward = area
backward :: Double -> AnyShape
backward x = AnyShape (Square (sqrt x))
Hablando estrictamente, no estás ganando o perdiendo ningún poder eligiendo uno contra el otro. Lo que significa que la elección debe basarse en otros factores como la facilidad de uso o el rendimiento.
Y tenga en cuenta que los tipos existenciales tienen otros usos fuera de este ejemplo de listas heterogéneas, por lo que es bueno tenerlos. Por ejemplo, el ST
tipo de Haskell , que nos permite escribir funciones que son externamente puras pero utilizan internamente operaciones de mutación de memoria, utiliza una técnica basada en tipos existenciales para garantizar la seguridad en el momento de la compilación.
Entonces, la respuesta general es que no hay una respuesta general. Los usos de los tipos existenciales solo pueden juzgarse en contexto, y las respuestas pueden ser diferentes dependiendo de las características y la sintaxis proporcionadas por los diferentes idiomas.