Perché una funzione con tipo polimorfico `forall t: Type, t-> t` deve essere la funzione di identità?


18

Sono nuovo alla teoria del linguaggio di programmazione. Stavo guardando alcune lezioni online in cui l'istruttore sosteneva che una funzione con tipo polimorfico forall t: Type, t->tfosse l'identità, ma non spiegava perché. Qualcuno può spiegarmi perché? Forse una prova dell'affermazione dei primi principi.


3
Ho pensato che questa domanda dovesse essere un duplicato, ma non riesco a trovarla. cs.stackexchange.com/questions/341/… è una sorta di follow-up. Il riferimento standard è Teoremi gratis! di Phil Wadler.
Gilles 'SO- smetti di essere malvagio' il

1
Prova a costruire una funzione generica con questo tipo che faccia qualsiasi altra cosa. Scoprirai che non ce n'è.
Bergi,

@Bergi Sì, non sono stato in grado di trovare alcun esempio contatore, ma non ero ancora sicuro di come dimostrarlo.
abhishek,

Ma quali sono state le tue osservazioni quando hai provato a trovarne una? Perché i tentativi che hai fatto non hanno funzionato?
Bergi,

@Gilles Forse ti ricordi cs.stackexchange.com/q/19430/14663 ?
Bergi,

Risposte:


32

La prima cosa da notare è che questo non è necessariamente vero. Ad esempio, a seconda della lingua, una funzione con quel tipo, oltre ad essere la funzione identità, potrebbe: 1 null) eseguire il ciclo per sempre, 2) mutare un certo stato, 3) restituire , 4) generare un'eccezione, 5) eseguire un I / O, 6) biforcare un thread per fare qualcos'altro, 7) fare call/ccshenanigans, 8) usare qualcosa come Java Object.hashCode, 9) usare reflection per determinare se il tipo è un numero intero e incrementarlo in tal caso, 10) usare reflection per analizzare lo stack di chiamate e fare qualcosa in base al contesto in cui viene chiamato, 11) probabilmente molte altre cose e certamente combinazioni arbitrarie di quanto sopra.

Quindi la proprietà che porta a questo, la parametricità, è una proprietà del linguaggio nel suo insieme e ci sono variazioni sempre più forti di esso. Per molti dei calcoli formali studiati nella teoria dei tipi, nessuno dei comportamenti sopra elencati può verificarsi. Ad esempio, per il Sistema F / il puro calcolo lambda polimorfico, in cui la parametria è stata studiata per la prima volta, non può verificarsi nessuno dei comportamenti sopra descritti. Semplicemente non ha eccezioni, stato mutevole, null, call/cc, I / O, la riflessione, ed è fortemente normalizzare in modo che non può ciclo per sempre. Come menzionato da Gilles in un commento, i teoremi di carta gratis!di Phil Wadler è una buona introduzione a questo argomento e i suoi riferimenti andranno oltre nella teoria, in particolare nella tecnica delle relazioni logiche. Quel link elenca anche alcuni altri articoli di Wadler sul tema della parametricità.

Poiché la parametricità è una proprietà del linguaggio, per dimostrarlo è necessario prima articolare formalmente il linguaggio e quindi un argomento relativamente complicato. L'argomento informale per questo caso particolare supponendo che siamo nel calcolo lambda polimorfico è che, dal momento che non sappiamo nulla, tnon possiamo eseguire alcuna operazione sull'input (ad esempio, non possiamo incrementarlo perché non sappiamo se è un numero) o creare un valore di quel tipo (per quanto ne sappiamo t= Void, un tipo senza valori). L'unico modo per produrre un valore di tipo tè restituire quello che ci viene dato. Non sono possibili altri comportamenti. Un modo per vedere questo è usare una forte normalizzazione e mostrare che esiste solo un termine di forma normale di questo tipo.


1
In che modo System F ha evitato loop infiniti che il sistema di tipi non è in grado di rilevare? Questo è classificato come irrisolvibile nel caso generale.
Joshua,

2
@Joshua - la prova di impossibilità standard per il problema di arresto inizia con l'assunto che in primo luogo esiste un ciclo infinito. Quindi invocarlo per chiedersi perché il Sistema F non abbia loop infiniti è un ragionamento circolare. Più in generale, il Sistema F non è quasi completo di Turing, quindi dubito che soddisfi le ipotesi di tale prova. È facilmente abbastanza debole per un computer per dimostrare che tutti i suoi programmi terminano (nessuna ricorsione, nessun ciclo while, solo molto debole per i cicli, ecc.).
Jonathan Cast

@Joshua: è irrisolvibile nel caso generale , che non preclude la sua risoluzione in molti casi speciali. In particolare, è stato dimostrato che ogni programma che risulta essere un tipo di sistema ben tipizzato si ferma: esiste una prova uniforme che funziona per tutti questi programmi. Ovviamente, questo significa che ci sono altri programmi che non possono essere digitati nel sistema F ...
cody,

15

La prova del reclamo è piuttosto complessa, ma se è quello che vuoi davvero, puoi leggere il documento originale di Reynolds sull'argomento.

L'idea chiave è che vale per le funzioni parametricamente polimorfiche , in cui il corpo di una funzione polimorfica è lo stesso per tutte le istanze monomorfe della funzione. In un tale sistema, non è possibile formulare ipotesi sul tipo di un parametro di tipo polimorfico e se l'unico valore nell'ambito ha un tipo generico, non c'è nulla a che fare con esso se non restituirlo o passarlo ad altre funzioni " ho definito, che a sua volta non può fare altro che restituirlo o passarlo .. .etc. Quindi, alla fine, tutto ciò che puoi fare è una catena di funzioni di identità prima di restituire il parametro.


8

Con tutti gli avvertimenti menzionati da Derek e ignorando i paradossi derivanti dall'uso della teoria degli insiemi, lasciatemi fare una prova nello spirito di Reynolds / Wadler.

Una funzione del tipo:

f :: forall t . t -> t

è una famiglia di funzioni indicizzate per tipo tftt .

L'idea è che, per definire formalmente le funzioni polimorfiche, non dovremmo trattare i tipi come insiemi di valori, ma piuttosto come relazioni. Tipi di base, come Intindurre relazioni di uguaglianza - ad esempio, due Intvalori sono correlati se sono uguali. Le funzioni sono correlate se associano valori correlati a valori correlati. Il caso interessante sono le funzioni polimorfiche. Associano i tipi correlati ai valori correlati.

fg

forall t . t -> t

StfSfSSStgtttfgfSgt

fstfSft

()()t()t((), c)ctf()ftf()()()ftcc()cftiodttfid .

Puoi trovare maggiori dettagli nel mio blog .


-2

EDIT: un commento sopra ha fornito il pezzo mancante. Alcune persone stanno deliberatamente giocando con lingue tutt'altro che turing. Non mi interessa esplicitamente tali lingue. Un linguaggio davvero non usurabile completo è una cosa folle e difficile da progettare. Tutto il resto si espande su ciò che accade cercando di applicare questi teoremi a un linguaggio completo.

Falso!

function f(a): forall t: Type, t->t
    function g(a): forall t: Type, t->t
       return (a is g) ? f : a
    return a is f ? g : a

dove l' isoperatore confronta due variabili per l'identità di riferimento. Cioè, contengono lo stesso valore. Non un valore equivalente, stesso valore. Le funzioni fe gsono equivalenti per alcune definizioni ma non sono le stesse.

Se questa funzione viene passata, restituisce qualcos'altro; altrimenti restituisce il suo input. Qualcos'altro ha lo stesso tipo di se stesso, quindi può essere sostituito. In altre parole, fnon è l'identità, perché f(f)ritorna g, mentre l'identità ritornerebbef .

Affinché il teorema lo sostenga, deve assumere la ridicola capacità di ridurre

function cantor(n, <z, a>) : forall t: t: Type int, <int, t> -> <int, t>
    return n > 1 ? cantor((n % 2 > 0) ? (n + 1) : n / 2, <z + 1, a>) : <z, a>
return cantor(1000, <0, a>)[1]¹

Se sei disposto ad assumere che tu possa presumere che l'inferenza del tipo molto più semplice possa essere gestita.

Se proviamo a limitare il dominio fino a quando il teorema regge, finiamo per doverlo limitare terribilmente lontano.

  • Funzionale puro (nessuno stato modificabile, nessun IO). OK, posso conviverci. Molto tempo vogliamo eseguire prove sulle funzioni.
  • Libreria standard vuota. meh.
  • No raisee noexit . Ora stiamo iniziando a essere vincolati.
  • Non esiste un tipo di fondo.
  • Il linguaggio ha una regola che consente al compilatore di comprimere la ricorsione infinita assumendo che debba terminare. Il compilatore può rifiutare una banale ricorsione infinita.
  • Il compilatore può fallire se presentato con qualcosa che non può essere provato in entrambi i modi. Ora la libreria standard non può accettare funzioni come argomenti. Boo.
  • Non c'è nil. Questo sta iniziando a diventare problematico. Abbiamo esaurito i modi per gestire 1 / 0.³
  • La lingua non può fare inferenze sul tipo di ramo e non ha una sostituzione per quando il programmatore può provare un'inferenza di tipo che la lingua non può. Questo è piuttosto male.

L'esistenza di entrambi gli ultimi due vincoli ha paralizzato la lingua. Mentre è ancora Turing completo, l'unico modo per far funzionare il suo scopo generale è quello di simulare una piattaforma interna che interpreta una lingua con requisiti più ampi.

¹ Se pensi che il compilatore possa dedurlo, prova questo

function fermat(z) : int -> int
    function pow(x, p)
        return p = 0 ? 1 : x * pow(x, p - 1)
    function f2(x, y, z) : int, int, int -> <int, int>
        left = pow(x, 5) + pow(y, 5)
        right = pow(z, 5)
        return left = right
            ? <x, y>
            : pow(x, 5) < right
                ? f2(x + 1, y, z)
                : pow(y, 5) < right
                    ? f2(2, y + 1, z)
                    : f2(2, 2, z + 1)
    return f2(2, 2, z)
function cantor(n, <z, a>) : forall t: t: Type int, <int, t> -> <int, t>
    return n > 1 ? cantor((n % 2 > 0) ? (n + 1) : n / 2, <z + 1, a>) : <z, a>
return cantor(fermat(3)[0], <0, a>)[1]

² La prova che il compilatore non può farlo dipende dall'accecamento. Possiamo usare più librerie per garantire che il compilatore non possa vedere il ciclo in una volta. Inoltre, possiamo sempre costruire qualcosa in cui il programma funzioni ma non può essere compilato perché il compilatore non può eseguire l'induzione nella memoria disponibile.

³ Qualcuno pensa che tu possa avere questo valore di ritorno zero senza tipi generici arbitrari che restituiscono zero. Questo paga una brutta penalità per la quale non ho visto un linguaggio efficace in grado di pagarlo.

function f(a, b, c): t: Type: t[],int,int->t
    return a[b/c]

non deve compilare. Il problema fondamentale è che l'indicizzazione dell'array di runtime non funziona più.


@Bergi: ho costruito un controesempio.
Giosuè,

1
Per favore, prenditi un momento per riflettere sulla differenza tra la tua risposta e le altre due. La frase di apertura di Derek è "La prima cosa da notare è che questo non è necessariamente vero". E poi spiega quali proprietà di una lingua lo rendono vero. jmite spiega anche cosa lo rende vero. Al contrario, la tua risposta fornisce un esempio in un linguaggio non specificato (e non comune) con spiegazione zero. (Qual è foilcomunque il quantificatore?) Questo non è affatto utile.
Gilles 'SO- smetti di essere malvagio' il

1
@DW: se a è f, il tipo di a è il tipo di f che è anche il tipo di g e quindi dovrebbe passare il typecheck. Se un vero compilatore lo cacciasse, userei il cast di runtime che le lingue reali hanno sempre per il sistema di tipo statico che lo sbaglia e non fallirebbe mai durante il runtime.
Giosuè,

2
Non è così che funziona un typechecker statico. Non controlla che i tipi corrispondano per un singolo input specifico. Esistono regole di tipo specifiche, che hanno lo scopo di garantire che la funzione verifichi il controllo su tutti gli input possibili. Se è necessario l'uso di un typecast, questa soluzione è molto meno interessante. Naturalmente, se si ignora il sistema di tipi, il tipo di una funzione non garantisce nulla - nessuna sorpresa lì!
DW

1
@DW: ti manca il punto. Ci sono informazioni sufficienti per il controllo del tipo statico per dimostrare che il codice è sicuro se ha avuto l'arguzia di trovarlo.
Giosuè,
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.