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