web-dev-qa-db-fra.com

Pourquoi les évaluateurs optimaux du λ-calcul sont-ils capables de calculer de grandes exponentiations modulaires sans formules?

Les nombres d'église sont un encodage des nombres naturels comme fonctions.

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

De façon ordonnée, vous pouvez exposer 2 numéros d'église en les appliquant simplement. Autrement dit, si vous appliquez 4 à 2, vous obtenez le numéro de l'église 16, ou 2^4. De toute évidence, cela n'est absolument pas pratique. Les nombres d'église ont besoin d'une quantité linéaire de mémoire et sont vraiment, vraiment lents. Calculer quelque chose comme 10^10 - auquel GHCI répond rapidement correctement - prendrait des années et ne pourrait pas de toute façon tenir la mémoire de votre ordinateur.

J'ai récemment expérimenté avec des évaluateurs optimaux λ. Lors de mes tests, j'ai accidentellement tapé ce qui suit sur ma calculatrice λ optimale:

10 ^ 10 % 13

C'était censé être une multiplication, pas une exponentiation. Avant de pouvoir bouger mes doigts pour abandonner désespérément le programme en cours d'exécution, il a répondu à ma demande:

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

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

Avec mon "alerte de bug" clignotante, je suis allé sur Google et j'ai vérifié, 10^10%13 == 3 effectivement. Mais la λ-calculatrice n'était pas censée trouver ce résultat, elle peut à peine stocker 10 ^ 10. J'ai commencé à le souligner, pour la science. Il m'a répondu instantanément 20^20%13 == 3, 50^50%13 == 4, 60^60%3 == 0. J'ai dû utiliser outils externes pour vérifier ces résultats, car Haskell lui-même n'a pas pu le calculer (en raison d'un débordement d'entier) (c'est si vous utilisez des nombres entiers et non des nombres entiers, bien sûr!). Poussant ses limites, c'était la réponse à 200^200%31:

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

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

Si nous avions une copie de l'univers pour chaque atom sur l'univers, et nous avions un ordinateur pour chaque atom que nous avions au total, nous ne pourrions pas enregistrer le numéro de l'église 200^200. Cela m'a incité à me demander si mon Mac était vraiment aussi puissant. Peut-être que l'évaluateur optimal a pu ignorer les branches inutiles et arriver directement à la réponse de la même manière qu'Haskell avec l'évaluation paresseuse. Pour tester cela, j'ai compilé le programme λ pour 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)

Cela génère correctement 1 (5 ^ 5 % 4) - mais jetez n'importe quoi au-dessus 10^10 et il sera bloqué, éliminant l'hypothèse.

l'évaluateur optimal que j'ai utilisé est un programme JavaScript non optimisé de 160 lignes qui n'incluait aucune sorte de calcul de module exponentiel - et la fonction de module lambda-calcul que j'ai utilisée était tout aussi simple:

(λab.(b(λcd.(c(λe.(d(λfg.(f(efg)))e))))(λc.(c(λde.e)))(λc.(a(b(λdef.(d(λg.(egf))))(λd.d)(λde.(ed)))(b(λde.d)(λd.d)(λd.d))))))

Je n'ai utilisé aucun algorithme ou formule arithmétique modulaire spécifique. Alors, comment l'évaluateur optimal peut-il arriver aux bonnes réponses?

127
MaiaVictor

Le phénomène vient de la quantité d'étapes de réduction bêta partagées, qui peuvent être radicalement différentes dans l'évaluation paresseuse de style Haskell (ou appel par valeur habituel, qui n'est pas si loin à cet égard) et dans Vuillemin-Lévy-Lamping- Kathail-Asperti-Guerrini et al…) évaluation "optimale". Il s'agit d'une fonctionnalité générale, complètement indépendante des formules arithmétiques que vous pourriez utiliser dans cet exemple particulier.

Partager signifie avoir une représentation de votre terme lambda dans lequel un "nœud" peut décrire plusieurs parties similaires du terme lambda réel que vous représentez. Par exemple, vous pouvez représenter le terme

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

en utilisant un graphe (acyclique dirigé) dans lequel il n'y a qu'une seule occurrence du sous-graphe représentant (\y.y)a, et deux arêtes ciblant ce sous-graphe. En termes Haskell, vous avez un thunk, que vous n'évaluez qu'une seule fois, et deux pointeurs vers ce thunk.

La mémorisation de style Haskell implémente le partage de sous-termes complets. Ce niveau de partage peut être représenté par des graphiques acycliques dirigés. Le partage optimal n'a pas cette restriction: il peut également partager des sous-termes "partiels", ce qui peut impliquer des cycles dans la représentation graphique.

Pour voir la différence entre ces deux niveaux de partage, considérons le terme

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

Si votre partage est limité à des sous-termes complets comme c'est le cas dans Haskell, vous ne pouvez avoir qu'une seule occurrence de \z.z, Mais les deux bêta-redex ici seront distincts: l'un est (\z.z) x Et l'autre est (\z.z) ((\z.z) x), et comme ce ne sont pas des termes égaux, ils ne peuvent pas être partagés. Si le partage de sous-termes partiels est autorisé, il devient possible de partager le terme partiel (\z.z) [] (Ce n'est pas seulement la fonction \z.z, Mais "la fonction \z.z Appliquée à quelque chose ), qui évalue en une étape juste quelque chose , quoi que cela est donc un graphique dans lequel un seul nœud représente les deux applications de \z.z en deux arguments distincts, et dans lequel ces deux applications peuvent être réduites en une seule étape. Remarquez qu'il existe un cycle sur ce nœud, puisque l'argument de la "première occurrence" est précisément la "deuxième occurrence". Enfin, avec un partage optimal vous pouvez passer de (un graphe représentant) \x. (\z.z) ((\z.z) x)) à (un graphe représentant) le résultat \x.x En une seule étape de réduction bêta (plus une comptabilité). C'est essentiellement ce qui se passe dans votre évaluateur optimal (et la représentation graphique est également ce qui empêche l'explosion de l'espace).

Pour des explications légèrement étendues, vous pouvez consulter l'article --- (Faible optimalité et signification du partage (ce qui vous intéresse est l'introduction et la section 4.1, et peut-être certains des pointeurs bibliographiques à la fin ).

Pour revenir à votre exemple, le codage des fonctions arithmétiques travaillant sur les entiers de l'Église est l'une des mines d'exemples "bien connus" où les évaluateurs optimaux peuvent mieux performer que les langues traditionnelles (dans cette phrase, bien connu signifie en fait qu'une poignée de les spécialistes connaissent ces exemples). Pour plus d'exemples de ce type, jetez un œil au document Safe Operators: Brackets Closed Forever par Asperti et Chroboczek (et d'ailleurs, vous trouverez ici des termes lambda intéressants qui ne sont pas typables EAL; donc Je vous encourage à jeter un œil aux oracles, à commencer par ce papier Asperti/Chroboczek).

Comme vous l'avez dit vous-même, ce type d'encodage n'est absolument pas pratique, mais il représente toujours une belle façon de comprendre ce qui se passe. Et permettez-moi de conclure avec un défi pour une enquête plus approfondie: serez-vous en mesure de trouver un exemple sur lequel une évaluation optimale de ces encodages soi-disant mauvais est en fait comparable à l'évaluation traditionnelle sur une représentation raisonnable des données? (pour autant que je sache, c'est une vraie question ouverte).

118
Thibaut Balabonski

Ce n'est pas une réponse, mais c'est une suggestion de l'endroit où vous pourriez commencer à chercher.

Il existe un moyen trivial de calculer les exponentiations modulaires dans peu d'espace, en particulier en réécrivant

(a * x ^ y) % z

comme

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

Si un évaluateur évalue ainsi et conserve le paramètre d'accumulation a sous sa forme normale, vous éviterez d'utiliser trop d'espace. Si en effet votre évaluateur est optimal, il ne doit probablement pas faire plus de travail que celui-ci, donc ne peut en particulier pas utiliser plus d'espace que le temps que celui-ci prend pour évaluer.

Je ne sais pas vraiment ce qu'est vraiment un évaluateur optimal, donc je crains de ne pas pouvoir rendre cela plus rigoureux.

7
Tom Ellis