¿Por qué (o por qué no) se consideran los tipos existenciales una mala práctica en la programación funcional?

43

¿Cuáles son algunas técnicas que podría utilizar para refactorizar constantemente el código eliminando la dependencia de los tipos existenciales? Por lo general, estos se usan para descalificar construcciones no deseadas de su tipo, así como para permitir el consumo con un mínimo conocimiento sobre el tipo dado (o eso es lo que entiendo).

¿Alguien ha encontrado una manera simple y consistente de eliminar la dependencia de estos en el código que aún mantiene algunos de los beneficios? ¿O al menos alguna forma de deslizarse en una abstracción que permita su eliminación sin requerir un cambio significativo de código para hacer frente a la alteración?

Puedes leer más sobre los tipos existenciales aquí ("si te atreves ...").

Petr Pudlák
fuente
8
@RobertHarvey Encontré la publicación del blog que me hizo pensar en esta pregunta la primera vez: Haskell Antipattern: Existential Typeclass . Además, el artículo que menciona Joey Adams describe algunos problemas con existenciales en la Sección 3.1. Si tiene argumentos contrarios, compártalos.
Petr Pudlák
55
@ PetrPudlák: Tenga en cuenta que el antipatrón no hay tipos existenciales en general, sino un uso particular de ellos cuando algo más simple y fácil (y mejor respaldado en Haskell) haría el mismo trabajo.
CA McCann
10
Si desea saber por qué el autor de una publicación de blog en particular expresó una opinión, entonces la persona que probablemente debería preguntar es el autor.
Eric Lippert
66
@Ptolomeo Eso es una cuestión de opinión. Usando Haskell durante años, no puedo imaginar el uso de un lenguaje funcional que no tenga un sistema de tipo fuerte.
Petr Pudlák
44
@Ptolomeo: el mantra de Haskell es "si compila funciona", el mantra de Erlangs es "déjalo colapsar". La escritura dinámica es buena como pegamento, pero personalmente no construiría cosas usando solo el pegamento.
Den

Respuestas:

8

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 Showinstancias:

{-# 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:

  1. La única operación útil que puede realizar en un AnyShapees obtener su área.
  2. Aún necesita usar el AnyShapeconstructor para incorporar uno de los tipos de formas al AnyShapetipo.

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 Shapetipo , y escribes funciones para convertir tus círculos y cuadrados en Shapes.


¡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 AnyShapetipo de arriba y Doubleson 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 STtipo 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.

sacundim
fuente
Eso no es un isomorfismo.
Ryan Reich
2

No estoy demasiado familiarizado con Haskell, por lo que intentaré responder la parte general de la pregunta como desarrollador funcional no académico de C #.

Después de leer un poco, resulta que:

  1. Los comodines de Java son similares a los tipos existenciales:

    Diferencia entre los tipos existenciales de Scala y el comodín de Java por ejemplo

  2. Los comodines no se implementan en C # completamente: se admite la variación genérica, pero la variación del sitio de llamada no:

    Genéricos C #: comodines

  3. Es posible que no necesite esta característica todos los días, pero cuando lo haga, la sentirá (por ejemplo, tener que introducir un tipo adicional para que las cosas funcionen):

    Comodines en restricciones genéricas de C #

Sobre la base de esta información, los tipos / comodines existenciales son útiles cuando se implementan correctamente y no hay nada de malo en ellos per se, pero probablemente se puedan usar de forma incorrecta al igual que otras características del lenguaje.

Guarida
fuente