Perché i valutatori ottimali del calcolo λ sono in grado di calcolare grandi esponenziali modulari senza formule?


135

I numeri di chiesa sono una codifica di numeri naturali come funzioni.

(\ f x  (f x))             -- church number 1
(\ f x  (f (f (f x))))     -- church number 3
(\ f x  (f (f (f (f x))))) -- church number 4

Ordinariamente, puoi esponenziare 2 numeri di chiesa semplicemente applicandoli. Cioè, se si applica da 4 a 2, si ottiene il numero della chiesa 16o2^4 . Ovviamente, questo è assolutamente poco pratico. I numeri delle chiese hanno bisogno di una quantità lineare di memoria e sono molto, molto lenti. Il calcolo di qualcosa del genere 10^10- a cui GHCI risponde rapidamente in modo corretto - richiederebbe anni e non potrebbe comunque contenere la memoria del computer.

Ultimamente ho sperimentato con valutatori λ ottimali. Nei miei test, ho accidentalmente digitato quanto segue sul mio calcolatore λ ottimale:

10 ^ 10 % 13

Doveva essere moltiplicazione, non esponenziazione. Prima che potessi muovere le dita per interrompere disperatamente il programma in esecuzione per sempre, rispose alla mia richiesta:

3
{ iterations: 11523, applications: 5748, used_memory: 27729 }

real    0m0.104s
user    0m0.086s
sys     0m0.019s

Con il mio "avviso bug" lampeggiante, sono andato su Google e verificato, 10^10%13 == 3anzi. Ma il calcolatore λ non doveva trovare quel risultato, riusciva a malapena a memorizzare 10 ^ 10. Ho iniziato a sottolinearlo, per la scienza. Mi ha risposto immediatamente 20^20%13 == 3, 50^50%13 == 4, 60^60%3 == 0. Ho dovuto usare strumenti esterni per verificare quei risultati, dal momento che Haskell stesso non era in grado di calcolarlo (a causa del trabocco di numeri interi) (è se usi Integer non Ints, ovviamente!). Spingendolo ai suoi limiti, questa era la risposta a 200^200%31:

5
{ iterations: 10351327, applications: 5175644, used_memory: 23754870 }

real    0m4.025s
user    0m3.686s
sys 0m0.341s

Se avessimo una copia dell'universo per ogni atomo nell'universo e avessimo un computer per ogni atomo che avevamo in totale, non potremmo memorizzare il numero della chiesa 200^200 . Questo mi ha spinto a chiedermi se il mio mac fosse davvero così potente. Forse il valutatore ottimale è stato in grado di saltare i rami non necessari e arrivare alla risposta nello stesso modo che Haskell fa con una valutazione pigra. Per provare questo, ho compilato il programma λ su Haskell:

data Term = F !(Term -> Term) | N !Double
instance Show Term where {
    show (N x) = "(N "++(if fromIntegral (floor x) == x then show (floor x) else show x)++")";
    show (F _) = "(λ...)"}
infixl 0 #
(F f) # x = f x
churchNum = F(\(N n)->F(\f->F(\x->if n<=0 then x else (f#(churchNum#(N(n-1))#f#x)))))
expMod    = (F(\v0->(F(\v1->(F(\v2->((((((churchNum # v2) # (F(\v3->(F(\v4->(v3 # (F(\v5->((v4 # (F(\v6->(F(\v7->(v6 # ((v5 # v6) # v7))))))) # v5))))))))) # (F(\v3->(v3 # (F(\v4->(F(\v5->v5)))))))) # (F(\v3->((((churchNum # v1) # (churchNum # v0)) # ((((churchNum # v2) # (F(\v4->(F(\v5->(F(\v6->(v4 # (F(\v7->((v5 # v7) # v6))))))))))) # (F(\v4->v4))) # (F(\v4->(F(\v5->(v5 # v4))))))) # ((((churchNum # v2) # (F(\v4->(F(\v5->v4))))) # (F(\v4->v4))) # (F(\v4->v4))))))) # (F(\v3->(((F(\(N x)->F(\(N y)->N(x+y)))) # v3) # (N 1))))) # (N 0))))))))
main = print $ (expMod # N 5 # N 5 # N 4)

Questo genera correttamente 1(5 ^ 5 % 4 ) - ma lancia qualcosa sopra 10^10e rimarrà bloccato, eliminando l'ipotesi.

Il valutatore ottimale che ho usato è un programma JavaScript lungo 160 righe non ottimizzato che non includeva alcun tipo di matematica del modulo esponenziale - e la funzione del modulo lambda-calcolo che ho usato era altrettanto semplice:

ab.(bcd.(ce.(dfg.(f(efg)))e))))(λc.(cde.e)))(λc.(a(bdef.(dg.(egf))))(λd.d)(λde.(ed)))(bde.d)(λd.d)(λd.d))))))

Non ho usato alcun algoritmo o formula aritmetica modulare specifica. Quindi, in che modo il valutatore ottimale è in grado di arrivare alle risposte giuste?


2
Puoi dirci di più sul tipo di valutazione ottimale che usi? Forse una citazione cartacea? Grazie!
Jason Dagit,

11
Sto usando l'algoritmo astratto di Lamping, come spiegato nel libro The Optimal Implementation of Functional Programming Languages . Nota che non sto usando "l'oracolo" (niente cornetti / parentesi) poiché quel termine è tipizzabile EAL. Inoltre, invece di ridurre casualmente i fan in parallelo, sto attraversando in sequenza il grafico per non ridurre i nodi irraggiungibili, ma temo che questo non sia in letteratura AFAIK ...
MaiaVictor

7
Va bene, nel caso qualcuno fosse curioso, ho creato un repository GitHub con il codice sorgente per il mio valutatore ottimale. Ha molti commenti e puoi provarlo in esecuzione node test.js. Fatemi sapere se avete domande.
MaiaVictor,

1
Neat find! Non so abbastanza sulla valutazione ottimale, ma posso dire che questo mi ricorda il piccolo teorema di Fermat / il teorema di Eulero. Se non lo conosci, potrebbe essere un buon punto di partenza.
Luqui,

5
Questa è la prima volta in cui non ho il minimo indizio su quale sia la domanda, ma ciononostante ho votato a fondo sulla domanda, e in particolare sull'eccezionale prima risposta post.
Marco13

Risposte:


124

Il fenomeno deriva dalla quantità di passaggi di riduzione beta condivisi, che possono essere drammaticamente diversi nella valutazione pigra in stile Haskell (o nella normale call-by-value, che non è così lontana da questo punto di vista) e in Vuillemin-Lévy-Lamping- Kathail-Asperti-Guerrini- (et al ...) valutazione "ottimale". Questa è una caratteristica generale, che è completamente indipendente dalle formule aritmetiche che potresti usare in questo esempio particolare.

Condividere significa avere una rappresentazione del termine lambda in cui un "nodo" può descrivere diverse parti simili del termine lambda che rappresenti. Ad esempio, puoi rappresentare il termine

\x. x ((\y.y)a) ((\y.y)a)

usando un grafico (aciclico diretto) in cui vi è una sola occorrenza del sottografo che rappresenta (\y.y)a e due bordi che prendono di mira quel sottografo. In termini di Haskell, hai un thunk, che valuti solo una volta, e due puntatori a questo thunk.

La memoizzazione in stile Haskell implementa la condivisione di sottrazioni complete. Questo livello di condivisione può essere rappresentato da grafici aciclici diretti. La condivisione ottimale non ha questa limitazione: può anche condividere sottotermi "parziali", che possono implicare cicli nella rappresentazione grafica.

Per vedere la differenza tra questi due livelli di condivisione, considera il termine

\x. (\z.z) ((\z.z) x)

Se la tua condivisione è limitata al completamento di sottere come nel caso di Haskell, potresti avere solo una ricorrenza di \z.z, ma i due beta-redex qui saranno distinti: uno è (\z.z) xe l'altro è (\z.z) ((\z.z) x), e poiché non sono termini uguali non possono essere condivisi. Se è consentita la condivisione di subtermini parziali, diventa possibile condividere il termine parziale (\z.z) [](che non è solo la funzione \z.z, ma "la funzione \z.zapplicata a qualcosa ), che valuta in un solo passaggio solo qualcosa , qualunque sia questo argomento. puoi avere un grafico in cui solo un nodo rappresenta le due applicazioni di\z.za due argomenti distinti e in cui queste due applicazioni possono essere ridotte in un solo passaggio. Si noti che esiste un ciclo su questo nodo, poiché l'argomento della "prima occorrenza" è precisamente la "seconda occorrenza". Infine, con una condivisione ottimale puoi passare da (un grafico che rappresenta) \x. (\z.z) ((\z.z) x))a (un grafico che rappresenta) il risultato\x.x in un solo passo di riduzione beta (oltre ad alcuni controlli contabili). Questo è fondamentalmente ciò che accade nel tuo valutatore ottimale (e la rappresentazione grafica è anche ciò che impedisce l'esplosione dello spazio).

Per spiegazioni leggermente estese, puoi consultare il documento Ottimizzazione debole e il significato della condivisione (ciò che ti interessa è l'introduzione e la sezione 4.1, e forse alcuni dei puntatori bibliografici alla fine).

Tornando al tuo esempio, la codifica delle funzioni aritmetiche che lavorano sugli interi della Chiesa è una delle miniere "ben note" di esempi in cui i valutatori ottimali possono eseguire meglio dei linguaggi tradizionali (in questa frase, ben noto in realtà significa che una manciata di gli specialisti sono a conoscenza di questi esempi). Per altri esempi simili, dai un'occhiata al documento Operatori sicuri: staffe chiuse per sempre di Asperti e Chroboczek (e, a proposito, troverai qui interessanti termini lambda che non sono tipizzabili con EAL; quindi ti incoraggio a prendere uno sguardo agli oracoli, a partire da questa carta Asperti / Chroboczek).

Come hai detto tu stesso, questo tipo di codifica è assolutamente poco pratico, ma rappresentano comunque un modo piacevole di capire cosa sta succedendo. E consentitemi di concludere con una sfida per ulteriori indagini: sarete in grado di trovare un esempio su quale valutazione ottimale su queste presunte codifiche errate sia effettivamente alla pari della valutazione tradizionale su una rappresentazione dei dati ragionevole? (per quanto ne so questa è una vera domanda aperta).


34
Questo è un primo post insolitamente accurato. Benvenuto in StackOverflow!
Dfeuer,

2
Niente di meno che perspicace. Grazie e benvenuto nella community!
MaiaVictor

7

Questa non è una risposta, ma è un suggerimento su dove potresti iniziare a cercare.

Esiste un modo banale per calcolare esponenziazioni modulari in poco spazio, in particolare riscrivendo

(a * x ^ y) % z

come

(((a * x) % z) * x ^ (y - 1)) % z

Se un valutatore valuta in questo modo e mantiene il parametro di accumulo ain forma normale, eviterai di usare troppo spazio. Se davvero il tuo valutatore lo è ottimale, presumibilmente non deve fare altro lavoro di questo, quindi in particolare non può usare più spazio del tempo impiegato da questo per valutare.

Non sono davvero sicuro di cosa sia davvero un valutatore ottimale, quindi temo di non poterlo rendere più rigoroso.


4
@Viclib Fibonacci come dice @Tom è un buon esempio. fibrichiede tempo esponenziale in modo ingenuo, che può essere ridotto a lineare con una semplice memorizzazione / programmazione dinamica. Anche il tempo logaritmico (!) È possibile calcolando l'n-esima potenza della matrice di [[0,1],[1,1]](purché si contenga ciascuna moltiplicazione per avere un costo costante).
chi

1
Anche il tempo costante se sei abbastanza audace per approssimarti :)
J. Abrahamson,

5
@TomEllis Perché qualcosa che sa solo come ridurre le espressioni arbitrarie di calcolo lambda potrebbe averne idea (a * b) % n = ((a % n) * b) % n? Questa è sicuramente la parte misteriosa.
Reid Barton,

2
@ReidBarton sicuramente l'ho provato! Stessi risultati, comunque.
MaiaVictor,

2
@ TomEllis e Chi, c'è solo una piccola osservazione, però. Tutto ciò presuppone che la tradizionale funzione ricorsiva sia l'implementazione fib "ingenua", ma IMO esiste un modo alternativo per esprimerlo che è molto più naturale. La forma normale di quella nuova rappresentazione ha la metà delle dimensioni di quella tradizionale) e Optlam riesce a calcolarla in modo lineare! Quindi direi che è la definizione "ingenua" di fib per quanto riguarda il calcolo λ.
Farei
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.