Perché (o perché no) i tipi esistenziali sono considerati una cattiva pratica nella programmazione funzionale?

42

Quali sono alcune tecniche che potrei usare per refactoring coerente del codice rimuovendo la dipendenza da tipi esistenziali? Tipicamente questi sono usati per squalificare le costruzioni indesiderate del tuo tipo e per consentire il consumo con una conoscenza minima del tipo dato (o almeno così è la mia comprensione).

Qualcuno ha escogitato un modo semplice e coerente per rimuovere la dipendenza da questi in codice che conserva ancora alcuni dei vantaggi? O almeno qualche modo di scivolare in un'astrazione che permetta la loro rimozione senza richiedere significativi codici di abbandono per far fronte all'alterazione?

Puoi leggere di più sui tipi esistenziali qui ("se ti azzardi ..").

    
posta Petr Pudlák 26.01.2013 - 23:35
fonte

2 risposte

8

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:

  1. L'unica operazione utile che puoi eseguire su AnyShape è ottenere la sua area.
  2. 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.

    
risposta data 05.03.2016 - 01:12
fonte
2

Non ho molta familiarità con Haskell, quindi cercherò di rispondere alla parte generale della domanda come sviluppatore C # funzionale non accademico.

Dopo aver fatto qualche lettura, risulta che:

  1. I caratteri jolly Java sono simili ai tipi esistenti:

    Differenza tra i tipi esistenziali di Scala e il jolly di Java con l'esempio

  2. I caratteri jolly non sono implementati completamente in C #: la varianza generica è supportata, ma la varianza del sito di chiamata non è:

    C # Generics: caratteri jolly

  3. Potresti non aver bisogno di questa funzione ogni giorno, ma quando lo senti lo sentirai (ad esempio dovendo introdurre un tipo extra per far funzionare le cose):

    Caratteri jolly in C # generici

Sulla base di questa informazione, i tipi / caratteri jolly sono utili se implementati correttamente e non c'è nulla di sbagliato in loro stessi, ma possono probabilmente essere utilizzati in modo improprio, proprio come le altre funzionalità linguistiche.

    
risposta data 13.03.2015 - 15:07
fonte

Leggi altre domande sui tag