Por que (ou porque não) os tipos existenciais são considerados uma prática ruim na programação funcional?

42

Quais são algumas técnicas que eu posso usar para refatorar de forma consistente o código, removendo a dependência de tipos existenciais? Normalmente, eles são usados para desqualificar construções indesejadas do seu tipo, bem como para permitir o consumo com um mínimo de conhecimento sobre o tipo dado (ou assim é o meu entendimento).

Alguém inventou uma maneira simples e consistente de remover a dependência deles em código que ainda mantém alguns dos benefícios? Ou, pelo menos, alguma maneira de escorregar em uma abstração que permita a sua remoção sem exigir um código significativo para lidar com a alteração?

Você pode ler mais sobre tipos existenciais aqui ("se você ousar ...").

    
por Petr Pudlák 26.01.2013 / 23:35
fonte

2 respostas

8

Tipos existenciais não são realmente considerados uma prática ruim na programação funcional. Eu acho que o que está te enganando é que um dos usos mais comumente citados para existenciais é o antipadrão de typeclass existencial , que muitas pessoas acreditam ser uma má prática.

Esse padrão é frequentemente apresentado como uma resposta à questão de como ter uma lista de elementos tipificados de forma heterogênea que implementam a mesma typeclass. Por exemplo, você pode querer ter uma lista de valores que tenham Show instâncias:

{-# 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)]

O problema com código como este é o seguinte:

  1. A única operação útil que você pode executar em um AnyShape é obter sua área.
  2. Você ainda precisa usar o construtor AnyShape para trazer um dos tipos de formas para o tipo AnyShape .

Por isso, esse pedaço de código não te dá nada que esse mais curto não faça:

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)]

No caso de classes multi-método, o mesmo efeito pode ser geralmente mais simples usando uma codificação "registro de métodos" - em vez de usar uma typeclass como Shape , você define um tipo de registro cujos campos são os "métodos" do tipo Shape e você escreve funções para converter seus círculos e quadrados em Shape s.

Mas isso não significa que tipos existenciais sejam um problema! Por exemplo, em Rust, eles têm um recurso chamado objetos de característica que as pessoas geralmente descrevem como um tipo existencial sobre uma característica (versões de Rust de classes de tipo). Se typeclasses existenciais são antipadrões em Haskell, isso significa que Rust escolheu uma solução ruim? Não! A motivação no mundo Haskell é sobre sintaxe e conveniência, não sobre princípios.

Uma maneira mais matemática de mostrar isso é que os tipos AnyShape acima e Double são isomórficos - há uma "conversão sem perdas" entre eles (bem, exceto para flutuante precisão pontual):

forward :: AnyShape -> Double
forward = area

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

Assim, estritamente falando, você não está ganhando ou perdendo nenhum poder escolhendo um contra o outro. O que significa que a escolha deve ser baseada em outros fatores, como facilidade de uso ou desempenho.

E tenha em mente que tipos existenciais têm outros usos fora desse exemplo de listas heterogêneas, então é bom tê-los. Por exemplo, o ST type de Haskell, que nos permite escrever funções que são externamente puras, mas usa internamente operações de mutação de memória, usa uma técnica baseada em tipos existenciais para garantir a segurança em tempo de compilação.

Portanto, a resposta geral é que não há resposta geral. Usos de tipos existenciais só podem ser julgados no contexto - e as respostas podem ser diferentes dependendo de quais recursos e sintaxe são fornecidos por diferentes idiomas.

    
por 05.03.2016 / 01:12
fonte
2

Eu não estou muito familiarizado com Haskell, então tentarei responder à parte geral da questão como um desenvolvedor C # funcional não acadêmico.

Depois de fazer algumas leituras, acontece que:

  1. Os curingas de Java são semelhantes aos tipos existenciais:

    Diferença entre os tipos existenciais de Scala e o curinga de Java pelo exemplo

  2. Os curingas não são implementados em C # completamente: a variação genérica é suportada, mas a variação do site de chamada não é:

    Generics C #: curingas

  3. Você pode não precisar desse recurso todos os dias, mas, quando fizer isso, sentirá isso (por exemplo, é necessário introduzir um tipo extra para fazer as coisas funcionarem):

    Curingas em restrições genéricas C #

Com base nessas informações, os tipos / curingas existenciais são úteis quando implementados corretamente e não há nada de errado com eles, mas eles provavelmente podem ser utilizados de maneira incorreta, como em outros recursos de linguagem.

    
por 13.03.2015 / 15:07
fonte