Warum können λ-Kalkül-optimale Bewerter große modulare Exponentiationen ohne Formeln berechnen?


135

Kirchenzahlen sind eine Kodierung natürlicher Zahlen als Funktionen.

(\ 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

Ordentlich können Sie 2 Kirchennummern potenzieren, indem Sie sie einfach anwenden. Das heißt, wenn Sie 4 bis 2 anwenden, erhalten Sie die Kirchennummer 16oder 2^4. Das ist natürlich völlig unpraktisch. Kirchenzahlen benötigen eine lineare Menge an Gedächtnis und sind sehr, sehr langsam. Das Berechnen von so etwas 10^10- was GHCI schnell richtig beantwortet - würde ewig dauern und könnte sowieso nicht in den Speicher Ihres Computers passen.

Ich habe in letzter Zeit mit optimalen λ-Bewertern experimentiert. Bei meinen Tests habe ich versehentlich Folgendes auf meinem optimalen λ-Rechner eingegeben:

10 ^ 10 % 13

Es sollte Multiplikation sein, keine Potenzierung. Bevor ich meine Finger bewegen konnte, um das ewig laufende Programm verzweifelt abzubrechen, beantwortete es meine Anfrage:

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

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

Mit blinkendem "Bug Alert" ging ich zu Google und überprüfte 10^10%13 == 3tatsächlich. Aber der λ-Rechner sollte dieses Ergebnis nicht finden, er kann kaum 10 ^ 10 speichern. Ich begann es für die Wissenschaft zu betonen. Es hat mich sofort beantwortet 20^20%13 == 3, 50^50%13 == 4, 60^60%3 == 0. Ich musste externe Tools verwenden , um diese Ergebnisse zu überprüfen, da Haskell selbst nicht in der Lage war, sie zu berechnen (aufgrund eines Überlaufs von Ganzzahlen) (wenn Sie natürlich Ganzzahlen und nicht Ints verwenden!). Dies war die Antwort auf 200^200%31:

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

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

Wenn wir eine Kopie des Universums für jedes Atom im Universum hätten und einen Computer für jedes Atom, das wir insgesamt hatten, könnten wir die Kirchennummer nicht speichern 200^200. Dies veranlasste mich zu der Frage, ob mein Mac wirklich so leistungsfähig war. Vielleicht konnte der optimale Bewerter die unnötigen Zweige überspringen und auf die gleiche Weise zur Antwort gelangen, wie es Haskell mit der faulen Bewertung tut. Um dies zu testen, habe ich das λ-Programm für Haskell kompiliert:

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)

Dies gibt 1( 5 ^ 5 % 4) korrekt aus - aber wirf irgendetwas darüber 10^10und es bleibt hängen, wodurch die Hypothese beseitigt wird.

Der optimale Evaluator, den ich verwendet habe, ist ein 160 Zeilen langes, nicht optimiertes JavaScript-Programm, das keinerlei Exponentialmodul-Mathematik enthielt - und die von mir verwendete Lambda-Kalkül-Modul-Funktion war ebenso einfach:

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))))))

Ich habe keinen bestimmten modularen arithmetischen Algorithmus oder eine Formel verwendet. Wie kann der optimale Bewerter zu den richtigen Antworten gelangen?


2
Können Sie uns mehr über die Art der optimalen Bewertung erzählen, die Sie verwenden? Vielleicht ein Papierzitat? Vielen Dank!
Jason Dagit

11
Ich verwende den abstrakten Algorithmus von Lamping, wie im Buch Die optimale Implementierung funktionaler Programmiersprachen erläutert . Beachten Sie, dass ich das "Orakel" (keine Croissants / Klammern) nicht verwende, da dieser Begriff EAL-typisierbar ist. Anstatt die Lüfter zufällig parallel zu reduzieren, durchlaufe ich den Graphen nacheinander, um nicht erreichbare Knoten nicht zu reduzieren, aber ich befürchte, dies ist nicht in der Literatur AFAIK ...
MaiaVictor

7
Okay, falls jemand neugierig ist, habe ich ein GitHub-Repository mit dem Quellcode für meinen optimalen Evaluator eingerichtet. Es hat viele Kommentare und Sie können es laufen testen node test.js. Lassen Sie mich wissen, wenn Sie Fragen haben.
MaiaVictor

1
Ordentlicher Fund! Ich weiß nicht genug über eine optimale Bewertung, aber ich kann sagen, dass dies mich an Fermats kleinen Satz / Eulers Satz erinnert. Wenn Sie sich dessen nicht bewusst sind, ist dies möglicherweise ein guter Ausgangspunkt.
Luqui

5
Dies ist das erste Mal, dass ich nicht die geringste Ahnung habe, worum es bei der Frage geht, aber dennoch die Frage und insbesondere die herausragende Antwort nach dem ersten Beitrag positiv bewertet habe.
Marco13

Antworten:


124

Das Phänomen ergibt sich aus der Anzahl der gemeinsamen Beta-Reduktionsschritte, die sich in der verzögerten Bewertung nach Haskell-Art (oder dem üblichen Call-by-Value, der in dieser Hinsicht nicht so weit entfernt ist) und in Vuillemin-Lévy-Lamping- dramatisch unterscheiden können. Kathail-Asperti-Guerrini- (et al…) "optimale" Bewertung. Dies ist eine allgemeine Funktion, die völlig unabhängig von den arithmetischen Formeln ist, die Sie in diesem speziellen Beispiel verwenden könnten.

Teilen bedeutet, eine Darstellung Ihres Lambda-Terms zu haben, in der ein "Knoten" mehrere ähnliche Teile des tatsächlichen Lambda-Terms beschreiben kann, den Sie darstellen. Sie können beispielsweise den Begriff darstellen

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

Verwenden eines (gerichteten azyklischen) Graphen, in dem nur ein Vorkommen des darstellenden Untergraphen (\y.y)aund zwei Kanten vorhanden sind, die auf diesen Untergraphen abzielen. In Haskell-Begriffen haben Sie einen Thunk, den Sie nur einmal auswerten, und zwei Zeiger auf diesen Thunk.

Die Memoisierung im Haskell-Stil implementiert die gemeinsame Nutzung vollständiger Subterms. Diese Ebene des Teilens kann durch gerichtete azyklische Graphen dargestellt werden. Die optimale Freigabe unterliegt dieser Einschränkung nicht: Sie kann auch "partielle" Subterme gemeinsam nutzen, was Zyklen in der Diagrammdarstellung implizieren kann.

Betrachten Sie den Begriff, um den Unterschied zwischen diesen beiden Ebenen des Teilens zu erkennen

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

Wenn Ihre Freigabe auf vollständige Subterms beschränkt ist, wie dies in Haskell der Fall ist, haben Sie möglicherweise nur ein Vorkommen von \z.z, aber die beiden Beta-Redexes hier sind unterschiedlich: einer ist (\z.z) xund der andere ist (\z.z) ((\z.z) x), und da sie nicht gleich sind Sie können nicht geteilt werden. Wenn das Teilen von Teilunterbedingungen zulässig ist, wird es möglich, den Teilbegriff (\z.z) [](das ist nicht nur die Funktion \z.z, sondern "die \z.zauf etwas angewendete Funktion ") zu teilen , der in einem Schritt nur etwas ergibt, unabhängig davon, um welches Argument es sich handelt Sie können ein Diagramm erstellen, in dem nur ein Knoten die beiden Anwendungen von darstellt\z.zauf zwei unterschiedliche Argumente, und in denen diese beiden Anwendungen in nur einem Schritt reduziert werden können. Beachten Sie, dass sich auf diesem Knoten ein Zyklus befindet, da das Argument des "ersten Auftretens" genau das "zweite Vorkommen" ist. Schließlich können Sie mit optimaler Freigabe in nur einem Schritt der Beta-Reduzierung (plus etwas Buchhaltung) von (einem Diagramm, das darstellt) \x. (\z.z) ((\z.z) x))zu (einem Diagramm, das darstellt) dem Ergebnis \x.xwechseln. Dies geschieht im Grunde genommen in Ihrem optimalen Evaluator (und die grafische Darstellung verhindert auch eine Raumexplosion).

Für etwas erweiterte Erklärungen können Sie sich das Papier Schwache Optimalität und die Bedeutung des Teilens ansehen (was Sie interessiert, ist die Einführung und der Abschnitt 4.1 und möglicherweise einige der bibliografischen Hinweise am Ende).

Zurück zu Ihrem Beispiel: Die Kodierung von Rechenfunktionen, die an Ganzzahlen der Kirche arbeiten, ist eine der "bekannten" Beispiele, in denen optimale Bewerter eine bessere Leistung als die gängigen Sprachen erbringen können (in diesem Satz bedeutet "bekannt" tatsächlich eine Handvoll von Fachleute kennen diese Beispiele). Weitere Beispiele finden Sie im Artikel Sichere Operatoren: Für immer geschlossene Klammern von Asperti und Chroboczek (und Sie finden hier übrigens interessante Lambda-Begriffe, die nicht EAL-typisierbar sind. Ich empfehle Ihnen daher, diese zu verwenden ein Blick auf Orakel, beginnend mit diesem Asperti / Chroboczek-Papier).

Wie Sie selbst sagten, ist diese Art der Codierung völlig unpraktisch, aber sie bietet immer noch eine gute Möglichkeit, zu verstehen, was vor sich geht. Lassen Sie mich mit einer Herausforderung für die weitere Untersuchung abschließen: Können Sie ein Beispiel finden, bei dem die optimale Bewertung dieser vermeintlich schlechten Codierungen tatsächlich der herkömmlichen Bewertung einer angemessenen Datendarstellung entspricht? (Soweit ich weiß, ist dies eine wirklich offene Frage).


34
Das ist ein ungewöhnlich gründlicher erster Beitrag. Willkommen bei StackOverflow!
Feuer

2
Nicht weniger als aufschlussreich. Vielen Dank und willkommen in der Community!
MaiaVictor

7

Dies ist keine Antwort, aber es ist ein Vorschlag, wo Sie anfangen könnten zu suchen.

Es gibt eine einfache Möglichkeit, modulare Exponentiationen auf kleinem Raum zu berechnen, insbesondere durch Umschreiben

(a * x ^ y) % z

wie

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

Wenn ein Evaluator so auswertet und den Akkumulationsparameter ain normaler Form hält, vermeiden Sie zu viel Speicherplatz. Wenn ja Ihr Bewerter ist optimal dann vermutlich muss es nicht mehr Arbeit als diese tut, so kann insbesondere nicht mehr Platz als die Zeit nutzen , dies zu bewerten nimmt.

Ich bin mir nicht sicher, was ein optimaler Bewerter wirklich ist, also fürchte ich, ich kann das nicht strenger machen.


4
@Viclib Fibonacci, wie @Tom sagt, ist ein gutes Beispiel. fiberfordert auf naive Weise exponentielle Zeit, die mit einer einfachen Memoisierung / dynamischen Programmierung auf linear reduziert werden kann. Sogar logarithmische (!) Zeit ist möglich, indem die n-te Matrixleistung von berechnet wird [[0,1],[1,1]](solange Sie jede Multiplikation zählen, um konstante Kosten zu haben).
Chi

1
Sogar konstante Zeit, wenn Sie es wagen, sich anzunähern :)
J. Abrahamson

5
@ TomEllis Warum sollte etwas, das nur weiß, wie man willkürliche Lambda-Kalkülausdrücke reduziert, eine Idee dazu haben (a * b) % n = ((a % n) * b) % n? Das ist sicherlich der mysteriöse Teil.
Reid Barton

2
@ReidBarton sicherlich habe ich es versucht! Gleiche Ergebnisse.
MaiaVictor

2
@ TomEllis und Chi, es gibt jedoch nur eine kleine Bemerkung. Das alles setzt voraus, dass die traditionelle rekursive Funktion die "naive" Fib-Implementierung ist, aber IMO gibt es eine alternative Art, sie auszudrücken, die viel natürlicher ist. Die normale Form dieser neuen Darstellung ist halb so groß wie die traditionelle), und Optlam schafft es, diese linear zu berechnen! Ich würde also argumentieren, dass dies die "naive" Definition von fib ist, was den λ-Kalkül betrifft. Ich würde einen Blog-Beitrag machen, aber ich bin nicht sicher, ob es sich wirklich lohnt ...
MaiaVictor
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.