I tipi esistenziali non sono realmente considerati cattive pratiche nella programmazione funzionale. Penso che ciò che ti ha inciampato sia che uno degli usi più comunemente citati per gli esistenziali è l' antipasto di tabella dei tipi esistenziali , che molte persone credono sia una cattiva pratica.
Questo modello viene spesso tracciato come risposta alla domanda su come avere un elenco di elementi tipizzati in modo eterogeneo che implementano tutti la stessa classe di caratteri. Ad esempio, potresti voler avere un elenco di valori con Show
istanze:
{-# 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 un
AnyShape
è ottenere la sua area.
- È ancora necessario utilizzare il
AnyShape
costruttore per inserire uno dei tipi di forma nel AnyShape
tipo.
Quindi, a quanto pare, quel pezzo di codice non ti dà davvero nulla di quello che questo più corto non ha:
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 in modo più semplice utilizzando una codifica "record di metodi", anziché utilizzare una classe di tipi come Shape
, si definisce un tipo di record i cui campi sono i "metodi" del Shape
tipo e scrivi 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 caratteristica chiamata oggetti tratto che le persone spesso descrivono come un tipo esistenziale rispetto a un tratto (versioni di Rust delle classi di tipi). Se i caratteri tipografici esistenziali sono un antipasto in Haskell, significa che Rust ha scelto una cattiva soluzione? No! La motivazione nel mondo Haskell riguarda la sintassi e la convenienza, non proprio i principi.
Un modo più matematico per dirlo consiste nel fatto che i AnyShape
tipi dall'alto e Double
sono isomorfi: esiste una "conversione senza perdita di dati" tra loro (beh, a parte la precisione in virgola mobile):
forward :: AnyShape -> Double
forward = area
backward :: Double -> AnyShape
backward x = AnyShape (Square (sqrt x))
Quindi, a rigor di termini, non stai guadagnando o perdendo alcun potere scegliendo l'uno contro l'altro. Ciò significa che la scelta dovrebbe essere basata su altri fattori come la facilità d'uso o le prestazioni.
E tieni presente che i tipi esistenziali hanno altri usi al di fuori di questo esempio di elenchi eterogenei, quindi è bene averli. Ad esempio, il ST
tipo di Haskell , che ci consente di scrivere funzioni esternamente pure ma che utilizzano internamente operazioni di mutazione della memoria, utilizza una tecnica basata su tipi esistenziali al fine di garantire sicurezza al momento della compilazione.
Quindi la risposta generale è che non esiste una risposta generale. Gli usi dei tipi esistenziali possono essere giudicati solo nel contesto e le risposte possono essere diverse a seconda delle caratteristiche e della sintassi fornite da lingue diverse.