Por que os avaliadores ideais do cálculo λ são capazes de calcular grandes exponenciações modulares sem fórmulas?


135

Os números das igrejas são uma codificação dos números naturais como funções.

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

Ordenadamente, você pode exponenciar dois números de igrejas apenas aplicando-os. Ou seja, se você aplicar 4 a 2, obterá o número da igreja 16ou 2^4. Obviamente, isso é totalmente impraticável. Os números das igrejas precisam de uma quantidade linear de memória e são muito, muito lentos. A computação de algo como 10^10- que o GHCI responde rapidamente corretamente - levaria séculos e não caberia na memória do seu computador.

Ultimamente tenho experimentado avaliadores λ ideais. Nos meus testes, digitei acidentalmente o seguinte na minha λ-calculadora ideal:

10 ^ 10 % 13

Era para ser multiplicação, não exponenciação. Antes que eu pudesse mover meus dedos para abortar o programa em execução eterna em desespero, ele respondeu ao meu pedido:

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

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

Com o meu "alerta de bug" piscando, fui ao Google e verifiquei, de 10^10%13 == 3fato. Mas a calculadora λ não deveria encontrar esse resultado, mal pode armazenar 10 ^ 10. Comecei a enfatizar isso, pela ciência. Ele imediatamente me respondeu 20^20%13 == 3, 50^50%13 == 4, 60^60%3 == 0. Eu tive que usar ferramentas externas para verificar esses resultados, já que o próprio Haskell não foi capaz de computá-lo (devido ao estouro de número inteiro) (é se você usar Inteiros, não Ints, é claro!). Levando-o ao limite, esta foi a resposta para 200^200%31:

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

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

Se tivéssemos uma cópia do universo para cada átomo no universo e tivéssemos um computador para cada átomo no total, não poderíamos armazenar o número da igreja 200^200. Isso me levou a questionar se meu Mac era realmente tão poderoso. Talvez o avaliador ideal tenha conseguido pular os ramos desnecessários e chegar diretamente à resposta da mesma maneira que Haskell faz na avaliação preguiçosa. Para testar isso, compilei o programa λ para 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)

Isso gera corretamente 1( 5 ^ 5 % 4) - mas atire qualquer coisa acima 10^10e ele ficará preso, eliminando a hipótese.

O avaliador ideal que usei é um programa JavaScript não otimizado de 160 linhas de comprimento que não inclui nenhum tipo de módulo de matemática exponencial - e a função do módulo lambda-calculus que usei era igualmente simples:

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

Não usei nenhum algoritmo ou fórmula aritmética modular específica. Então, como o avaliador ideal é capaz de chegar às respostas certas?


2
Você pode nos dizer mais sobre o tipo de avaliação ideal que você usa? Talvez uma citação em papel? Obrigado!
Jason Dagit 29/07

11
Estou usando o algoritmo abstrato de Lamping, conforme explicado no livro A implementação ideal de linguagens de programação funcional . Observe que não estou usando o "oráculo" (sem croissants / colchetes), pois esse termo é tipável por EAL. Além disso, em vez de reduzir aleatoriamente fãs em paralelo, estou atravessando sequencialmente o gráfico a não reduzir os nós inacessíveis, mas tenho medo isto não é sobre a literatura AFAIK ...
MaiaVictor

7
Ok, caso alguém esteja curioso, configurei um repositório GitHub com o código-fonte para o meu avaliador ideal. Tem muitos comentários e você pode testá-lo em execução node test.js. Deixe-me saber se você tiver alguma dúvida.
MaiaVictor

1
Puro encontrar! Não sei o suficiente sobre a avaliação ideal, mas posso dizer que isso me lembra o Pequeno Teorema de Fermat / Teorema de Euler. Se você não souber, pode ser um bom ponto de partida.
luqui 29/07

5
Esta é a primeira vez que eu não tenho a menor idéia do que se trata a pergunta, mas, mesmo assim, vota a questão e, em particular, a excelente resposta do primeiro post.
Marco13

Respostas:


124

O fenômeno vem da quantidade de etapas compartilhadas de redução beta, que podem ser dramaticamente diferentes na avaliação preguiçosa ao estilo de Haskell (ou na chamada usual por valor, que não é tão distante a esse respeito) e em Vuillemin-Lévy-Lamping- Kathail-Asperti-Guerrini- (et al.) Avaliação "ótima". Esse é um recurso geral, completamente independente das fórmulas aritméticas que você pode usar neste exemplo em particular.

Compartilhar significa ter uma representação do seu termo lambda no qual um "nó" pode descrever várias partes semelhantes do termo lambda real que você representa. Por exemplo, você pode representar o termo

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

usando um gráfico (acíclico direcionado) no qual há apenas uma ocorrência do subgráfico representando (\y.y)ae duas arestas direcionadas para esse subgráfico. Em termos de Haskell, você tem um thunk, que você avalia apenas uma vez, e dois ponteiros para esse thunk.

A memorização no estilo Haskell implementa o compartilhamento de subtermos completos. Esse nível de compartilhamento pode ser representado por gráficos acíclicos direcionados. O compartilhamento ideal não possui essa restrição: ele também pode compartilhar subtermos "parciais", o que pode implicar ciclos na representação gráfica.

Para ver a diferença entre esses dois níveis de compartilhamento, considere o termo

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

Se o seu compartilhamento estiver restrito a subtermos completos, como é o caso em Haskell, você poderá ter apenas uma ocorrência de \z.z, mas os dois redexos beta aqui serão distintos: um é (\z.z) xe o outro é (\z.z) ((\z.z) x)e, uma vez que não são termos iguais. eles não podem ser compartilhados. Se o compartilhamento de subtermos parciais for permitido, torna-se possível compartilhar o termo parcial (\z.z) [](que não é apenas a função \z.z, mas "a função \z.zaplicada a algo ), que avalia em uma etapa apenas algo , qualquer que seja esse argumento. você pode ter um gráfico no qual apenas um nó representa as duas aplicações de\z.za dois argumentos distintos e nos quais esses dois aplicativos podem ser reduzidos em apenas uma etapa. Observe que há um ciclo nesse nó, pois o argumento da "primeira ocorrência" é precisamente a "segunda ocorrência". Por fim, com o compartilhamento ideal, você pode ir de (um gráfico representando) \x. (\z.z) ((\z.z) x))para (um gráfico representando) o resultado \x.xem apenas uma etapa da redução beta (mais alguma contabilidade). Isso é basicamente o que acontece no seu avaliador ideal (e a representação gráfica também é o que impede a explosão do espaço).

Para explicações um pouco mais amplas, você pode olhar para o artigo Optimalidade Fraca e o Significado do Compartilhamento (o que você está interessado é na introdução e na seção 4.1, e talvez alguns dos indicadores bibliográficos no final).

Voltando ao seu exemplo, a codificação de funções aritméticas que trabalham com números inteiros da Igreja é uma das minas "bem conhecidas" de exemplos em que os avaliadores ótimos podem ter um desempenho melhor do que os idiomas comuns (nesta frase, bem conhecido na verdade significa que um punhado de especialistas estão cientes desses exemplos). Para mais exemplos desse tipo, dê uma olhada no artigo Safe Operators: Brackets Closed Forever de Asperti e Chroboczek (e, a propósito, você encontrará aqui termos lambda interessantes que não são compatíveis com a EAL; portanto, estou incentivando você a uma olhada nos oráculos, começando com este artigo de Asperti / Chroboczek).

Como você mesmo disse, esse tipo de codificação é totalmente impraticável, mas eles ainda representam uma boa maneira de entender o que está acontecendo. E deixe-me concluir com um desafio para uma investigação mais aprofundada: você será capaz de encontrar um exemplo em que a avaliação ideal dessas codificações supostamente ruins está realmente no mesmo nível da avaliação tradicional em uma representação de dados razoável? (tanto quanto sei, essa é uma questão realmente aberta).


34
Esse é o primeiro post mais incomumente completo. Bem-vindo ao StackOverflow!
Dfeuer

2
Nada menos que perspicaz. Obrigado e bem-vindo à comunidade!
MaiaVictor 31/07

7

Isso não é uma resposta, mas é uma sugestão de onde você pode começar a procurar.

Existe uma maneira trivial de calcular exponenciações modulares em pouco espaço, especificamente reescrevendo

(a * x ^ y) % z

Como

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

Se um avaliador avaliar dessa forma e manter o parâmetro acumulador ana forma normal, você evitará usar muito espaço. Se, de fato, seu avaliador é ideal, presumivelmente, ele não deve fazer mais trabalho que este, portanto, em particular, não pode usar mais espaço do que o tempo que leva para avaliar.

Não tenho muita certeza do que realmente é um avaliador ideal, por isso tenho medo de não tornar isso mais rigoroso.


4
@Vibib Fibonacci, como @Tom diz, é um bom exemplo. fibrequer tempo exponencial de maneira ingênua, que pode ser reduzida a linear com uma simples memorização / programação dinâmica. Mesmo o tempo logarítmico (!) É possível computando a n-ésima potência da matriz de [[0,1],[1,1]](desde que você conte cada multiplicação para ter um custo constante).
chi

1
Mesmo constante de tempo, se você está desafiando o suficiente para aproximadamente :)
J. Abrahamson

5
@ TomEllis Por que algo que apenas sabe reduzir expressões arbitrárias de cálculo lambda tem alguma idéia disso (a * b) % n = ((a % n) * b) % n? Essa é a parte misteriosa com certeza.
Reid Barton

2
@ReidBarton certamente eu tentei! Mesmos resultados, no entanto.
MaiaVictor

2
@ TomEllis e Chi, há apenas uma pequena observação. Tudo isso pressupõe que a função recursiva tradicional é a implementação "ingênua" da fib, mas na IMO existe uma maneira alternativa de expressá-la que é muito mais natural. A forma normal dessa nova representação tem metade do tamanho da tradicional) e o Optlam consegue calculá-la linearmente! Então, eu argumentaria que essa é a definição "ingênua" de fib no que diz respeito ao cálculo-λ. Eu faria um post no blog, mas não tenho certeza se vale a pena ...
MaiaVictor
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.