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

42

¿Cuáles son algunas de las técnicas que podría usar para refactorizar constantemente el código y eliminar la dependencia de los tipos existenciales? Normalmente, estos se utilizan para descalificar construcciones no deseadas de su tipo, así como para permitir el consumo con un conocimiento mínimo sobre el tipo dado (o eso es mi entendimiento).

¿Alguien ha encontrado una manera simple y consistente de eliminar la confianza en 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 una pérdida significativa de código para hacer frente a la alteración?

Puede leer más sobre los tipos existenciales aquí ("si se atreve ...").

    
pregunta Petr Pudlák 26.01.2013 - 23:35

2 respuestas

8

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:

  1. La única operación útil que puede realizar en un AnyShape es obtener su área.
  2. 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.

    
respondido por el sacundim 05.03.2016 - 01:12
2

No estoy muy familiarizado con Haskell, así que intentaré responder la parte general de la pregunta como un 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 completamente en C #: se admite la varianza genérica, pero la varianza del sitio de llamada no:

    C # Generics: Wildcards

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

    Wildcards en las 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, pero es probable que se utilicen incorrectamente al igual que en otras características del idioma.

    
respondido por el Den 13.03.2015 - 15:07

Lea otras preguntas en las etiquetas