I tipi esistenziali non sono realmente considerati cattive pratiche nella programmazione funzionale. Penso che quello che ti sta facendo inciampare è che uno degli usi più comunemente citati per gli esistenziali è antipattern esistenziale esemplare , che molte persone credono sia una cattiva pratica.
Questo schema è spesso trotto come risposta alla domanda su come avere un elenco di elementi tipizzati in modo eterogeneo che implementano tutti la stessa classe di tipizzazione. Ad esempio, potresti avere un elenco di valori con istanze di Show
:
{-# 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)]
Il problema con codice come questo è questo:
- L'unica operazione utile che puoi eseguire su
AnyShape
è ottenere la sua area.
- Devi ancora usare il costruttore di
AnyShape
per portare uno dei tipi di forma nel tipo AnyShape
.
Quindi, a quanto risulta, quella parte di codice non ti dà davvero niente che non sia questa più corta:
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)]
Nel caso di classi multi-metodo, lo stesso effetto può essere generalmente ottenuto più semplicemente usando una codifica "record of methods", invece di usare una classe di caratteri come Shape
, si definisce un tipo di record i cui campi sono i "metodi" del tipo Shape
e scrivi le funzioni per convertire le tue cerchie e i tuoi quadrati in Shape
s.
Ma ciò non significa che i tipi esistenziali siano un problema! Ad esempio, in Rust hanno una funzione chiamata oggetti tratto che le persone spesso descrivono come un tipo esistenziale su un tratto (le versioni di Rust delle classi di tipi). Se i typeclass esistenziali sono un antipattern in Haskell, vuol dire che Rust ha scelto una cattiva soluzione? No! La motivazione nel mondo di Haskell riguarda la sintassi e la convenienza, non solo i principi.
Un modo più matematico di mettere questo è sottolineare che il AnyShape
digita dall'alto e Double
sono isomorfo - c'è una "conversione senza perdita" tra di loro (beh, salva per floating precisione del punto):
forward :: AnyShape -> Double
forward = area
backward :: Double -> AnyShape
backward x = AnyShape (Square (sqrt x))
Quindi, in senso stretto, non stai guadagnando o perdendo alcun potere scegliendo l'uno contro l'altro. Il che significa che la scelta deve essere basata su altri fattori come la facilità d'uso o le prestazioni.
E ricorda che i tipi esistenziali hanno altri usi al di fuori di questo esempio di liste eterogenee, quindi è bene averli. Ad esempio, il tipo di haskell ST
, che ci consente di scrivere funzioni esternamente pure ma che utilizzano internamente le operazioni di mutazione della memoria, utilizza una tecnica basata su tipi esistenziali per garantire la sicurezza in fase di compilazione.
Quindi la risposta generale è che non c'è una risposta generale. Gli usi di tipi esistenziali possono essere giudicati solo in un contesto e le risposte possono essere diverse a seconda di quali caratteristiche e sintassi sono fornite da lingue diverse.