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 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 é este:
- A única operação útil que você pode executar em um
AnyShape
é obter sua área.
- Você ainda precisa usar o
AnyShape
construtor para trazer um dos tipos de forma para o AnyShape
tipo.
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 Shape
tipo , 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, 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 AnyShape
tipo 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 ST
tipo 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.