Los tipos existenciales no son realmente considerados una mala práctica en la programación funcional. Creo que lo que te hace tropezar es que uno de los usos más citados de los existenciales es existencial typeclass antipattern , que mucha gente cree que es una mala práctica.
Este patrón a menudo se elimina 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 necesitas usar el constructor
AnyShape
para poner uno de los tipos de forma en el tipo AnyShape
.
Resulta que ese fragmento de código realmente no te ofrece 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 más simplemente usando una codificación de "registro de métodos"; en lugar de usar una clase de tipos como Shape
, usted define un tipo de registro cuyos campos son los "métodos" del tipo Shape
, y escribe funciones para convertir sus círculos y cuadrados en Shape
s.
¡Pero eso no significa que los tipos existenciales sean un problema! Por ejemplo, en Rust tienen una función llamada objetos de rasgo que las personas a menudo describen como un tipo existencial sobre un rasgo (versiones de clases de tipo de Rust). Si las clases de tipos existenciales son antipatternas en Haskell, ¿significa eso que Rust escogió una mala solución? ¡No! La motivación en el mundo de Haskell tiene que ver con la sintaxis y la conveniencia, no realmente con los principios.
Una forma más matemática de poner esto es señalar que el tipo AnyShape
desde arriba y Double
son isomorfos —hay una "conversión sin pérdida" entre ellos (bueno, excepto para flotar precisión del punto):
forward :: AnyShape -> Double
forward = area
backward :: Double -> AnyShape
backward x = AnyShape (Square (sqrt x))
Estrictamente hablando, no estás ganando o perdiendo ningún poder al elegir uno frente al 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 tipo ST
de Haskell, que nos permite escribir funciones que son externamente puras pero que 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 se pueden juzgar en contexto, y las respuestas pueden ser diferentes según las características y la sintaxis que proporcionan los diferentes idiomas.