Por que (ou por que não) os tipos existenciais são considerados más práticas em programação funcional?

43

Quais são algumas técnicas que eu poderia usar para refatorar consistentemente 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 especificado (ou seja, é o meu entendimento).

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

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

Petr Pudlák
fonte
8
@RobertHarvey Encontrei o post do blog que me fez pensar sobre essa questão pela primeira vez: Haskell Antipattern: Existential Typeclass . Além disso, o artigo que Joey Adams menciona descreve alguns problemas com existenciais na Seção 3.1. Se você tiver argumentos contrários, compartilhe-os.
Petr Pudlák
5
@ PetrPudlák: Lembre-se de que o antipadrão não existe tipos existenciais em geral, mas um uso específico deles quando algo mais simples e fácil (e com melhor suporte em Haskell) faria o mesmo trabalho.
CA McCann
10
Se você quiser saber por que o autor de um post em particular expressou uma opinião, a pessoa que você provavelmente deve perguntar é o autor.
Eric Lippert
6
@Ptolemy Isso é uma questão de opinião. Usando Haskell há anos, mal consigo imaginar usar uma linguagem funcional que não tenha um sistema de tipos forte.
Petr Pudlák
4
@Ptolomeu: O mantra de Haskell é "se ele compilar funciona", o mantra de Erlangs é "deixá-lo falhar". A digitação dinâmica é boa como cola, mas eu pessoalmente não construiria coisas usando apenas a cola.
Den

Respostas:

8

Tipos existentes não são realmente considerados uma prática ruim em programação funcional. Eu acho que o que está enganando você é que um dos usos mais citados para os existenciais é o antipadrão de classe tipográfica existencial , que muitas pessoas acreditam ser uma má prática.

Esse padrão é frequentemente traçado como uma resposta à questão de como ter uma lista de elementos de tipo heterogêneo que implementam a mesma classe de tipo. Por exemplo, você pode querer ter uma lista de valores que possuem Showinstâ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 é este:

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

Então, como se vê, esse trecho de código realmente não oferece nada que este menor não:

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 com vários métodos, o mesmo efeito pode geralmente ser realizado mais simplesmente usando uma codificação "registro de métodos" - em vez de usar uma classe de tipo Shape, você define um tipo de registro cujos campos são os "métodos" do Shapetipo , e você escreve funções para converter seus círculos e quadrados em Shapes.


Mas isso não significa que tipos existenciais sejam um problema! Por exemplo, no 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 classes de tipos de Rust). Se as classes de tipo existenciais são um antipadrão 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 realmente sobre princípios.

Uma maneira mais matemática de colocar isso é apontar que o AnyShapetipo de cima e Doubleé isomórfico - há uma "conversão sem perdas" entre eles (bem, exceto pela precisão do ponto flutuante):

forward :: AnyShape -> Double
forward = area

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

Então, estritamente falando, você não está ganhando ou perdendo poder ao escolher um contra o outro. O que significa que a escolha deve se basear em outros fatores, como facilidade de uso ou desempenho.


E lembre-se de que tipos existenciais têm outros usos fora deste exemplo de lista heterogênea; portanto, é bom tê-los. Por exemplo, o STtipo de Haskell , que nos permite escrever funções que são externamente puras, mas usam internamente operações de mutação de memória, usa uma técnica baseada em tipos existenciais para garantir a segurança no momento da compilação.

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

sacundim
fonte
Isso não é um isomorfismo.
Ryan Reich
2

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

Depois de fazer algumas leituras, verifica-se que:

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

    Diferença entre os tipos existenciais do Scala e o curinga do Java por exemplo

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

    C # Generics: Curingas

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

    Curingas em restrições genéricas de C #

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

Den
fonte