C'è un motivo per avere un tipo di fondo in un linguaggio di programmazione?


49

Un tipo di fondo è un costrutto che appare principalmente nella teoria dei tipi matematica. Si chiama anche il tipo vuoto. È un tipo che non ha valori, ma è un sottotipo di tutti i tipi.

Se il tipo restituito di una funzione è il tipo inferiore, significa che non restituisce. Periodo. Forse gira per sempre, o forse genera un'eccezione.

Qual è il punto di avere questo strano tipo in un linguaggio di programmazione? Non è così comune, ma è presente in alcuni, come Scala e Lisp.


2
@SargeBorsch: ne sei sicuro? Naturalmente in C non è possibile definire esplicitamente un voiddato ...
Basile Starynkevitch

3
@BasileStarynkevitch non ci sono valori di tipo voide il tipo di unità deve avere un valore. Inoltre, come hai sottolineato, non puoi nemmeno dichiarare un valore di tipo void, il che significa che non è nemmeno un tipo, solo un caso d'angolo speciale nella lingua.
Sarge Borsch,

2
Sì, la C è bizzarra in questo, specialmente nel modo in cui sono scritti i tipi di puntatore e funzione. Ma voidin Java è quasi lo stesso: non è proprio un tipo e non può avere valori.
Sarge Borsch,

3
Nella semantica delle lingue con un tipo di fondo, il tipo di fondo non è considerato privo di valori, ma piuttosto di avere un valore, il valore di fondo, che rappresenta un calcolo che non viene mai completato (normalmente). Poiché il valore inferiore è un valore di ogni tipo, il tipo inferiore può essere un sottotipo di ogni tipo.
Theodore Norvell,

4
@BasileStarynkevitch Common Lisp ha il tipo nil che non ha valori. Ha anche il tipo null che ha un solo valore, il simbolo nil(aka, ()), che è un tipo di unità.
Joshua Taylor,

Risposte:


33

Prenderò un semplice esempio: C ++ vs Rust.

Ecco una funzione utilizzata per generare un'eccezione in C ++ 11:

[[noreturn]] void ThrowException(char const* message,
                                 char const* file,
                                 int line,
                                 char const* function);

Ed ecco l'equivalente in Rust:

fn formatted_panic(message: &str, file: &str, line: isize, function: &str) -> !;

Su una questione puramente sintattica, il costrutto Rust è più sensato. Si noti che il costrutto C ++ specifica un tipo restituito anche se specifica anche che non restituirà. È un po 'strano.

Su una nota standard, la sintassi C ++ è apparsa solo con C ++ 11 (era trattata in cima), ma vari compilatori avevano fornito varie estensioni per un po ', quindi gli strumenti di analisi di terze parti dovevano essere programmati per riconoscere i vari modi questo attributo potrebbe essere scritto. Avere standardizzato è ovviamente chiaramente superiore.


Ora, per quanto riguarda il vantaggio?

Il fatto che una funzione non ritorni può essere utile per:

  • ottimizzazione: è possibile eliminare qualsiasi codice dopo di esso (non restituirà), non è necessario salvare i registri (in quanto non sarà necessario ripristinarli), ...
  • analisi statica: elimina una serie di potenziali percorsi di esecuzione
  • manutenibilità: (vedi analisi statica, ma da parte dell'uomo)

6
voidnell'esempio C ++ definisce (parte di) il tipo di funzione, non il tipo restituito. Limita il valore a cui è consentita la funzione return; tutto ciò che può convertire in vuoto (che è nulla). Se la funzione returns non deve essere seguita da un valore. Il tipo completo della funzione è void () (char const*, char const*, int, char const *). + 1 per l'utilizzo char constinvece di const char:-)
Più chiaro il

4
Ciò non significa che abbia più senso avere un tipo di fondo, ma solo che ha senso annotare le funzioni sul fatto che ritornino o meno come parte del linguaggio. In realtà, poiché le funzioni non possono tornare a causa di motivi diversi, sembra meglio codificare il motivo in qualche modo invece di usare un termine generico, un po 'come il concetto relativamente recente di annotazione delle funzioni basato sui loro effetti collaterali.
GregRos,

2
In realtà, c'è un motivo per rendere "non restituisce" e "ha il tipo di ritorno X" indipendente: compatibilità all'indietro per il proprio codice, poiché la convenzione di chiamata potrebbe dipendere dal tipo di ritorno.
Deduplicatore

è [[noreturn]] par della sintassi o un'aggiunta di funzionalità?
Zaibis,

1
[cont.] Nel complesso, direi solo che una discussione sui vantaggi di ⊥ deve definire ciò che si qualifica come implementazione di ⊥; e non credo che un sistema di tipi che non abbia ( a → ⊥) ≤ ( ab ) sia un'implementazione utile di ⊥. Quindi, in questo senso, SysV x86-64 C ABI (tra gli altri) non consente l'implementazione ⊥.
Alex Shpilkin,

26

La risposta di Karl è buona. Ecco un ulteriore uso che non penso che sia stato menzionato da nessun altro. Il tipo di

if E then A else B

dovrebbe essere un tipo che include tutti i valori nel tipo di Ae tutti i valori nel tipo di B. Se il tipo di Bè Nothing, il tipo ifdell'espressione può essere il tipo di A. Dichiarerò spesso una routine

def unreachable( s:String ) : Nothing = throw new AssertionError("Unreachable "+s) 

per dire che il codice non dovrebbe essere raggiunto. Dal momento che il suo tipo è Nothing, unreachable(s)ora può essere utilizzato in qualsiasi ifo (più spesso) switchsenza influire sul tipo di risultato. Per esempio

 val colour : Colour := switch state of
         BLACK_TO_MOVE: BLACK
         WHITE_TO_MOVE: WHITE
         default: unreachable("Bad state")

Scala ha un tale tipo di niente.

Un altro caso d'uso per Nothing(come menzionato nella risposta di Karl) è Elenco [Niente] è il tipo di elenchi ciascuno dei cui membri ha il tipo Nulla. Quindi può essere il tipo di elenco vuoto.

La proprietà chiave Nothingche fa funzionare questi casi d'uso non è che non abbia valori - sebbene in Scala, ad esempio, non abbia valori - è che è un sottotipo di ogni altro tipo.

Supponiamo di avere una lingua in cui ogni tipo contiene lo stesso valore: chiamiamolo (). In tale linguaggio il tipo di unità, che ha ()come unico valore, potrebbe essere un sottotipo di ogni tipo. Ciò non lo rende un tipo di fondo, nel senso che significava OP; l'OP era chiaro che un tipo di fondo non contiene valori. Tuttavia, poiché è un tipo che è un sottotipo di ogni tipo, può svolgere lo stesso ruolo di un tipo di fondo.

Haskell fa le cose in modo leggermente diverso. In Haskell, un'espressione che non produce mai un valore può avere lo schema dei tipi forall a.a. Un'istanza di questo schema di tipi si unificherà con qualsiasi altro tipo, quindi agisce efficacemente come tipo di fondo, anche se (standard) Haskell non ha alcuna nozione di sottotipo. Ad esempio, la errorfunzione del preludio standard ha uno schema di tipi forall a. [Char] -> a. Quindi puoi scrivere

if E then A else error ""

e il tipo di espressione sarà lo stesso del tipo di A, per qualsiasi espressione A.

L'elenco vuoto in Haskell ha lo schema dei tipi forall a. [a]. If Aè un'espressione il cui tipo è un tipo di elenco, quindi

if E then A else []

è un'espressione con lo stesso tipo di A.


Qual è la differenza tra il tipo forall a . [a]e il tipo [a]in Haskell? Le variabili di tipo non sono già quantificate universalmente nelle espressioni di tipo Haskell?
Giorgio,

@Giorgio In Haskell la quantificazione universale è implicita se è chiaro che stai guardando uno schema di tipi. Non puoi nemmeno scrivere forallnello standard Haskell 2010. Ho scritto esplicitamente la quantificazione perché questo non è un forum Haskell e alcune persone potrebbero non avere familiarità con le convenzioni di Haskell. Quindi non c'è differenza se non che forall a . [a]non è standard mentre lo [a]è.
Theodore Norvell,

19

I tipi formano un monoide in due modi, facendo insieme un semiring . Questo è ciò che si chiama tipi di dati algebrici . Per i tipi finiti, questo semiring si riferisce direttamente al semina di numeri naturali (incluso zero), il che significa che si contano quanti possibili valori ha il tipo (esclusi i "valori non terminanti").

  • Il tipo in basso (lo chiamerò Vacuous) ha zero valori .
  • Il tipo di unità ha un valore. Chiamerò sia il tipo che il suo valore singolo ().
  • La composizione (che la maggior parte dei linguaggi di programmazione supporta abbastanza direttamente, attraverso record / strutture / classi con campi pubblici) è un'operazione del prodotto . Per esempio, (Bool, Bool)ha quattro valori possibili, vale a dire (False,False), (False,True), (True,False)e (True,True).
    Il tipo di unità è l'elemento di identità dell'operazione di composizione. Ad esempio ((), False)e ((), True)sono gli unici valori di tipo ((), Bool), quindi questo tipo è isomorfo a Boolse stesso.
  • I tipi alternativi sono in qualche modo trascurati nella maggior parte delle lingue (le lingue OO le supportano con eredità), ma non sono meno utili. Un'alternativa tra due tipi Ae Bsostanzialmente ha tutti i valori di A, oltre a tutti i valori di B, quindi il tipo di somma . Per esempio, Either () Boolha tre valori, li chiamerò Left (), Right Falsee Right True.
    Il tipo in basso è l'elemento identità della somma: Either Vacuous Aha solo valori del modulo Right a, perché Left ...non ha senso ( Vacuousnon ha valori).

La cosa interessante di questi monoidi è che, quando introduci funzioni nella tua lingua, la categoria di questi tipi con le funzioni come morfismi è una categoria monoidale . Tra le altre cose, ciò consente di definire funzioni e monadi applicative , che si rivelano un'ottima astrazione per calcoli generali (che possono comportare effetti collaterali, ecc.) In termini altrimenti puramente funzionali.

Ora, in realtà puoi andare abbastanza lontano preoccupandoti solo di una parte del problema (la composizione monoidale), quindi non hai davvero bisogno del tipo di fondo esplicitamente. Ad esempio, anche Haskell per lungo tempo non aveva un tipo di fondo standard. Ora ha, si chiama Void.

Ma quando consideri il quadro completo, come una categoria chiusa bicartesiana , allora il sistema di tipi è in realtà equivalente all'intero calcolo lambda, quindi in pratica hai l'astrazione perfetta su tutto il possibile in un linguaggio completo di Turing. Ottimo per linguaggi specifici del dominio incorporato, ad esempio esiste un progetto sulla codifica diretta dei circuiti elettronici in questo modo .

Certo, potresti ben dire che questa è una sciocchezza generale di tutti i teorici . Non è necessario conoscere la teoria delle categorie per essere un buon programmatore, ma quando lo fai, ti dà modi potenti e ridicolmente generali di ragionare sul codice e di creare invarianti.


mb21 mi ricorda di notare che ciò non deve essere confuso con i valori inferiori . In lingue pigre come Haskell, ogni tipo contiene un "valore" inferiore, indicato . Questa non è una cosa concreta che potresti mai esplicitamente passare, ma è ciò che è "restituito", ad esempio quando una funzione scorre per sempre. Anche il Voidtipo di Haskell "contiene" il valore inferiore, quindi il nome. Alla luce di ciò, il tipo di fondo di Haskell ha davvero un valore e il suo tipo di unità ha due valori, ma nella discussione sulla teoria delle categorie questo è generalmente ignorato.


"Il tipo in basso (lo chiamerò Void)", che non deve essere confuso con il valore bottom , che è un membro di qualsiasi tipo in Haskell .
mb21

18

Forse gira per sempre, o forse genera un'eccezione.

Sembra un tipo utile da avere in quelle situazioni, per quanto rare possano essere.

Inoltre, anche se Nothing(il nome di Scala per il tipo in basso) non può avere valori, List[Nothing]non ha quella restrizione, che lo rende utile come il tipo di un elenco vuoto. La maggior parte delle lingue aggira questo creando un elenco vuoto di stringhe di tipo diverso rispetto a un elenco vuoto di numeri interi, il che ha un senso, ma rende un elenco vuoto più prolisso da scrivere, il che è un grosso svantaggio in un linguaggio orientato all'elenco.


12
"La lista vuota di Haskell è un costruttore di tipi": sicuramente la cosa rilevante al riguardo qui è più che è polimorfica o sovraccarica - cioè, le liste vuote di diversi tipi sono valori distinti, ma []rappresentano tutti loro, e saranno istaniate il tipo specifico, se necessario.
Peter LeFanu Lumsdaine,

È interessante notare che: Se si tenta di creare una matrice vuota nell'interprete Haskell, si ottiene un valore molto preciso con un tipo molto indefinito: [a]. Allo stesso modo, i :t Left 1rendimenti Num a => Either a b. La valutazione effettiva dell'espressione forza il tipo di a, ma non di b:Either Integer b
John Dvorak,

5
L'elenco vuoto è un costruttore di valori . Un po 'confuso, il costruttore del tipo coinvolto ha lo stesso nome ma l'elenco vuoto stesso è un valore non un tipo (beh, ci sono anche elenchi a livello di tipo, ma questo è un altro argomento). La parte che fa funzionare elenco vuoto per qualsiasi tipo di lista è la implicito forallnel suo genere, forall a. [a]. Ci sono alcuni modi carini a cui pensare forall, ma ci vuole del tempo per capire davvero.
David

@PeterLeFanuLumsdaine Questo è esattamente ciò che significa essere un costruttore di tipi. Significa solo che è un tipo con un tipo diverso da *.
GregRos,

2
In Haskell []è un costruttore di tipi ed []è un'espressione che rappresenta un elenco vuoto. Ma ciò non significa che "l'elenco vuoto di Haskell è un costruttore di tipi". Il contesto chiarisce se []viene utilizzato come tipo o come espressione. Supponiamo che tu dichiari data Foo x = Foo | Bar x (Foo x); ora puoi usare Foocome costruttore di tipo o come valore, ma è solo un caso che tu abbia scelto lo stesso nome per entrambi.
Theodore Norvell,

3

È utile per l'analisi statica documentare il fatto che un determinato percorso di codice non è raggiungibile. Ad esempio se si scrive quanto segue in C #:

int F(int arg) {
 if (arg != 0)
  return arg + 1; //some computation
 else
  Assert(false); //this throws but the compiler does not know that
}
void Assert(bool cond) { if (!cond) throw ...; }

Il compilatore si lamenterà che Fnon restituisce nulla in almeno un percorso di codice. Se Assertdovesse essere contrassegnato come non restituito, il compilatore non avrebbe bisogno di avvisare.


2

In alcune lingue, nullha il tipo di fondo, dal momento che il sottotipo di tutti i tipi definisce bene per quali lingue si usa null (nonostante la lieve contraddizione di nullessere sia se stesso che una funzione che ritorna se stessa, evitando gli argomenti comuni sul perché botdovrebbero essere disabitati).

Può anche essere utilizzato come catch-all nei tipi di funzione ( any -> bot) per gestire la spedizione andata male.

E alcune lingue ti consentono di risolvere effettivamente botcome un errore, che può essere utilizzato per fornire errori del compilatore personalizzati.


11
No, un tipo inferiore non è il tipo di unità. Un tipo bottom non ha alcun valore, quindi una funzione che restituisce un tipo bottom non deve restituire (cioè generare un'eccezione o un loop indefinitamente)
Basile Starynkevitch

@BasileStarynkevitch - Non sto parlando del tipo di unità. Il tipo di unità è mappato voidin lingue comuni (anche se con semantica leggermente diversa per lo stesso uso), no null. Anche se hai anche ragione sul fatto che la maggior parte delle lingue non modella null come tipo in basso.
Telastyn,

3
@TheodoreNorvell - le prime versioni di Tangent lo hanno fatto - anche se io sono l'autore, quindi forse è un imbroglio. Non ho i collegamenti salvati per gli altri, ed è passato un po 'di tempo da quando ho fatto quella ricerca.
Telastyn,

1
@Martijn Ma puoi usare null, ad esempio, un confronto tra un puntatore e nullun risultato booleano. Penso che le risposte mostrino che ci sono due tipi distinti di tipi di fondo. (a) Lingue (ad es. Scala) in cui il tipo che è un sottotipo di ogni tipo rappresenta calcoli che non forniscono alcun risultato. Essenzialmente è un tipo vuoto, sebbene tecnicamente spesso popolato da un valore inferiore inutile che rappresenta il nonterminazione. (b) Lingue come Tangente, in cui il tipo inferiore è un sottoinsieme di ogni altro tipo perché contiene un valore utile che si trova anche in ogni altro tipo: null.
Theodore Norvell,

4
È interessante notare che alcune lingue hanno un valore con un tipo che non puoi dichiarare (comune per il valore letterale null) e altre hanno un tipo che puoi dichiarare ma non ha valori (un tipo di fondo tradizionale) e che ricoprono ruoli un po 'comparabili .
Martijn,

1

Sì, questo è un tipo abbastanza utile; mentre il suo ruolo sarebbe principalmente interno al sistema di tipi, ci sono alcune occasioni in cui il tipo di fondo appare apertamente.

Prendi in considerazione un linguaggio tipicamente statico in cui i condizionali sono espressioni (quindi la costruzione if-then-else raddoppia come operatore ternario di C e amici, e potrebbe esserci un'affermazione a caso multipla simile). Il linguaggio di programmazione funzionale ha questo, ma succede anche in alcuni linguaggi imperativi (sin da ALGOL 60). Quindi tutte le espressioni di ramo devono in definitiva produrre il tipo dell'intera espressione condizionale. Si potrebbe semplicemente richiedere che i loro tipi siano uguali (e penso che questo sia il caso dell'operatore ternario in C), ma ciò è eccessivamente restrittivo, specialmente quando il condizionale può anche essere usato come istruzione condizionale (non restituendo alcun valore utile). In generale si vuole che ogni espressione di ramo sia (implicitamente) convertibile a un tipo comune che sarà il tipo dell'espressione completa (possibilmente con restrizioni più o meno complicate per consentire a quel tipo comune di essere efficacemente trovato dal complier, cfr. C ++, ma non entrerò in quei dettagli qui).

Esistono due tipi di situazioni in cui un tipo generale di conversione consentirà la necessaria flessibilità di tali espressioni condizionali. Uno è già menzionato, in cui il tipo di risultato è il tipo di unitàvoid; questo è naturalmente un super-tipo di tutti gli altri tipi e consentire a qualsiasi tipo di essere (banalmente) convertito in esso rende possibile usare l'espressione condizionale come istruzione condizionale. L'altro riguarda casi in cui l'espressione restituisce un valore utile, ma uno o più rami non sono in grado di produrne uno. Solitamente solleveranno un'eccezione o comporteranno un salto e richiedere loro di (anche) produrre un valore del tipo dell'intera espressione (da un punto irraggiungibile) sarebbe inutile. È questo tipo di situazione che può essere gestita con garbo dando clausole, salti e chiamate che generano eccezioni che avranno un tale effetto, il tipo in basso, l'unico tipo che può essere (banalmente) convertito in qualsiasi altro tipo.

Suggerirei di scrivere un tipo così basso da *suggerire la sua convertibilità in tipo arbitrario. Può servire internamente ad altri scopi utili, ad esempio quando si cerca di dedurre un tipo di risultato per una funzione ricorsiva che non ne dichiara alcuna, l'inferenziatore del tipo potrebbe assegnare il tipo *a qualsiasi chiamata ricorsiva per evitare una situazione di gallina e uovo; il tipo effettivo sarà determinato da rami non ricorsivi e quelli ricorsivi saranno convertiti nel tipo comune di rami non ricorsivi. Se ci sono presenti rami non ricorsivi affatto, il tipo rimarrà *, e indicano correttamente che la funzione ha alcun modo possibile di mai ritorno dal ricorsione. Oltre a questo e come tipo di risultato delle funzioni di lancio delle eccezioni, si può usare*come tipo di componente di sequenze di lunghezza 0, ad esempio dell'elenco vuoto; di nuovo se mai un elemento viene selezionato da un'espressione di tipo [*](necessariamente elenco vuoto), il tipo risultante *indicherà correttamente che questo non può mai tornare senza un errore.


Quindi l'idea che var foo = someCondition() ? functionReturningBar() : functionThatAlwaysThrows()potrebbe inferire il tipo di fooas Bar, dato che l'espressione non potrebbe mai produrre nient'altro?
supercat

1
Hai appena descritto il tipo di unità, almeno nella prima parte della tua risposta. Una funzione che restituisce il tipo di unità è la stessa di quella dichiarata come ritorno voidin C. La seconda parte della tua risposta, in cui parli di un tipo per una funzione che non ritorna mai, o di un elenco senza elementi - che è davvero il tipo di fondo! (È spesso scritto come _|_anziché *. Non so perché. Forse perché sembra un fondo (umano) :)
Andrewf

2
A scanso di equivoci: "non restituisce nulla di utile" è diverso da "non restituisce"; il primo è rappresentato dal tipo di unità; il secondo dal tipo in basso.
andrewf

@andrewf: Sì, capisco la distinzione. La mia risposta è un po 'lunga, ma il punto che volevo sottolineare è che il tipo di unità e il tipo di fondo svolgono entrambi ruoli (diversi ma) comparabili nel consentire a determinate espressioni di essere utilizzate in modo più flessibile (ma comunque in sicurezza).
Marc van Leeuwen,

@supercat: Sì, questa è l'idea. Attualmente in C ++ che è illegale, anche se sarebbe valida se functionThatAlwaysThrows()sono stati sostituiti da un esplicito throw, a causa di linguaggio speciale nello Standard. Avere un tipo che fa questo sarebbe un miglioramento.
Marc van Leeuwen,

0

In alcune lingue, è possibile annotare una funzione per dire sia al compilatore che agli sviluppatori che non verrà restituita una chiamata a questa funzione (e se la funzione è scritta in modo da poter essere restituita, il compilatore non lo consentirà ). Questa è una cosa utile da sapere, ma alla fine puoi chiamare una funzione come questa come qualsiasi altra. Il compilatore può utilizzare le informazioni per l'ottimizzazione, per fornire avvisi sul codice morto e così via. Quindi non esiste un motivo molto convincente per avere questo tipo, ma neppure un motivo molto convincente per evitarlo.

In molte lingue, una funzione può restituire "nulla". Cosa significa esattamente dipende dalla lingua. In C significa che la funzione non restituisce nulla. In Swift, significa che la funzione restituisce un oggetto con un solo valore possibile e poiché esiste un solo valore possibile, quel valore accetta zero bit e in realtà non richiede alcun codice. In entrambi i casi, non è lo stesso di "bottom".

"bottom" sarebbe un tipo senza valori possibili. Non può mai esistere. Se una funzione restituisce "bottom", in realtà non può tornare, perché non può restituire alcun valore di tipo "bottom".

Se un designer linguistico ne ha voglia, allora non c'è motivo di non avere quel tipo. L'implementazione non è difficile (puoi implementarla esattamente come una funzione che restituisce nulla e contrassegnata come "non restituisce"). Non è possibile mescolare i puntatori a funzioni che ritornano in basso con i puntatori a funzioni che restituiscono nulla, perché non sono dello stesso tipo).

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.