Por qual mecanismo essa função de fibonacci é memorizada?
fib = (map fib' [0..] !!)
where fib' 1 = 1
fib' 2 = 1
fib' n = fib (n-2) + fib (n-1)
E em uma nota relacionada, por que esta versão não é?
fib n = (map fib' [0..] !! n)
where fib' 1 = 1
fib' 2 = 1
fib' n = fib (n-2) + fib (n-1)
haskell
lazy-evaluation
fibonacci
memoization
pointfree
bjornars
fonte
fonte
fib 0
não termina: você provavelmente quer que os casos-basefib'
sejamfib' 0 = 0
efib' 1 = 1
.fibs = 1:1:zipWith (+) fibs (tail fibs)
efib = (fibs !!)
.Respostas:
O mecanismo de avaliação em Haskell é por necessidade : quando um valor é necessário, ele é calculado e mantido pronto para o caso de ser solicitado novamente. Se definirmos alguma lista,
xs=[0..]
e depois pedirmos seu 100º elemento,,xs!!99
o 100º slot na lista será "desenvolvido", segurando o número99
agora, pronto para o próximo acesso.Isso é o que aquele truque, "passar por uma lista", está explorando. Na definição normal de Fibonacci duplamente recursiva
fib n = fib (n-1) + fib (n-2)
, a própria função é chamada, duas vezes a partir do topo, causando a explosão exponencial. Mas, com esse truque, estabelecemos uma lista para os resultados provisórios e examinamos "a lista":O truque é fazer com que essa lista seja criada e que essa lista não desapareça (por meio de coleta de lixo) entre as chamadas para
fib
. A maneira mais fácil de fazer isso é nomear essa lista. "Se você der um nome, ele vai ficar."Sua primeira versão define uma constante monomórfica e a segunda define uma função polimórfica. Uma função polimórfica não pode usar a mesma lista interna para diferentes tipos que ela pode precisar servir, portanto, nenhum compartilhamento , ou seja, nenhuma memoização.
Com a primeira versão, o compilador está sendo generoso conosco, retirando essa subexpressão constante (
map fib' [0..]
) e tornando-a uma entidade compartilhável separada, mas não tem nenhuma obrigação de fazer isso. e há casos em que não queremos que isso aconteça para nós automaticamente.( editar :) Considere estas reescritas:
Portanto, a verdadeira história parece ser sobre as definições de escopo aninhadas. Não há escopo externo com a 1ª definição, e a 3ª tem o cuidado de não chamar o escopo externo
fib3
, mas o mesmo nívelf
.Cada nova invocação de
fib2
parece criar suas definições aninhadas novamente, porque qualquer uma delas poderia (em teoria) ser definida de forma diferente dependendo do valor den
(graças a Vitus e Tikhon por apontar isso). Com a primeira definição não há don
que depender e com a terceira há uma dependência, mas cada chamada separada parafib3
chamadas emf
que tem o cuidado de chamar apenas as definições do escopo de mesmo nível, interno a esta invocação específica defib3
, para que o mesmoxs
seja reutilizado (isto é, compartilhado) para essa invocação defib3
.Mas nada impede o compilador de reconhecer que as definições internas em qualquer uma das versões acima são de fato independentes da
n
ligação externa , para realizar o levantamento de lambda, afinal, resultando em memoização completa (exceto para as definições polimórficas). Na verdade, isso é exatamente o que acontece com todas as três versões quando declaradas com tipos monomórficos e compiladas com o sinalizador -O2. Com declarações de tipo polimórfico,fib3
exibe compartilhamento local efib2
nenhum compartilhamento.Em última análise, dependendo de um compilador e das otimizações do compilador usadas e de como você o testa (carregando arquivos em GHCI, compilados ou não, com -O2 ou não, ou autônomo), e se obtém um tipo monomórfico ou polimórfico, o comportamento pode mude completamente - se exibe compartilhamento local (por chamada) (ou seja, tempo linear em cada chamada), memoização (ou seja, tempo linear na primeira chamada e tempo 0 nas chamadas subsequentes com o mesmo argumento ou menor), ou nenhum compartilhamento ( tempo exponencial).
A resposta curta é, é uma coisa do compilador. :)
fonte
fib'
é redefinido para cadan
e, portanto,fib'
emfib 1
≠fib'
emfib 2
, o que implica também as listas são diferentes. Mesmo se você corrigir o tipo para ser monomórfico, ele ainda exibirá esse comportamento.where
cláusulas introduzem compartilhamento muito parecido comlet
expressões, mas tendem a esconder problemas como este. Reescrevendo-o um pouco mais explicitamente, você obtém isto: hpaste.org/71406Int -> Integer
), entãofib2
é executado em tempo exponencial,fib1
efib3
ambos são executados em tempo linear, masfib1
também são memoizados - novamente porquefib3
as definições locais são redefinidas para cadan
.pwr (x:xs) = pwr xs ++ map (x:) pwr xs ; pwr [] = [[]]
queremospwr xs
ser calculados independentemente, duas vezes, para que possa ser coletado como lixo em tempo real à medida que é produzido e consumido.Não estou totalmente certo, mas aqui está um palpite:
O compilador assume que
fib n
poderia ser diferente em um diferenten
e, portanto, precisaria recalcular a lista a cada vez. Afinal, os bits dentro dawhere
declaração podem dependern
. Ou seja, neste caso, toda a lista de números é essencialmente uma função den
.A versão sem
n
pode criar a lista uma vez e envolvê-la em uma função. A lista não pode depender do valor den
passado e isso é fácil de verificar. A lista é uma constante que é então indexada. É, claro, uma constante que é avaliada preguiçosamente, então seu programa não tenta obter a lista inteira (infinita) imediatamente. Por ser uma constante, ela pode ser compartilhada nas chamadas de função.É memorizado porque a chamada recursiva precisa apenas procurar um valor em uma lista. Como a
fib
versão cria a lista uma vez preguiçosamente, ela apenas calcula o suficiente para obter a resposta sem fazer cálculos redundantes. Aqui, "preguiçoso" significa que cada entrada na lista é uma conversão (uma expressão não avaliada). Quando você não avaliar a conversão, torna-se um valor, então acessá-lo na próxima vez que não repetir o cálculo. Como a lista pode ser compartilhada entre chamadas, todas as entradas anteriores já são calculadas no momento em que você precisa da próxima.É essencialmente uma forma inteligente e barata de programação dinâmica baseada na semântica preguiçosa do GHC. Acho que o padrão especifica apenas que não deve ser estrito , portanto, um compilador compatível poderia potencialmente compilar esse código para não memoize. No entanto, na prática, todo compilador razoável será preguiçoso.
Para obter mais informações sobre por que o segundo caso funciona, leia Compreendendo uma lista definida recursivamente (fibs em termos de zipWith) .
fonte
fib' n
poderia ser diferente em um diferenten
", talvez?fib
, inclusivefib'
, pode ser diferente em cada diferenten
. Eu acho que o exemplo original é um pouco confuso porquefib'
também depende dele próprion
que sombreia o outron
.Primeiro, com ghc-7.4.2, compilado com
-O2
, a versão não memoised não é tão ruim, a lista interna de números Fibonacci ainda memoised para cada chamada de nível superior para a função. Mas não é, e não pode ser razoavelmente, memoised em diferentes chamadas de nível superior. No entanto, para a outra versão, a lista é compartilhada entre as chamadas.Isso se deve à restrição do monomorfismo.
O primeiro é limitado por uma associação de padrão simples (apenas o nome, sem argumentos), portanto, pela restrição de monomorfismo, deve obter um tipo monomórfico. O tipo inferido é
e tal restrição é padronizada (na ausência de uma declaração padrão dizendo o contrário) para
Integer
, fixando o tipo comoPortanto, há apenas uma lista (do tipo
[Integer]
) para memorizar.O segundo é definido com um argumento de função, portanto, permanece polimórfico e, se as listas internas fossem memoizadas entre as chamadas, uma lista teria que ser memoizada para cada tipo em
Num
. Isso não é prático.Compile as duas versões com a restrição de monomorfismo desabilitada ou com assinaturas de tipo idênticas e ambas exibirão exatamente o mesmo comportamento. (Isso não era verdade para as versões mais antigas do compilador, não sei qual versão fez isso primeiro.)
fonte
fib 1000000
vários tipos, isso consumirá uma tonelada de memória. Para evitar isso, seria necessária uma heurística que listasse itens para serem descartados do cache quando ele se tornasse muito grande. E essa estratégia de memoisação também se aplicaria a outras funções ou valores, presumivelmente, de modo que o compilador teria que lidar com um número potencialmente grande de coisas para memorizar para muitos tipos em potencial. Acho que seria possível implementar o memoisation polimórfico (parcial) com uma heurística razoavelmente boa, mas duvido que valha a pena.Você não precisa da função memoize para Haskell. Apenas a linguagem de programação empirativa precisa dessas funções. No entanto, Haskel é uma linguagem funcional e ...
Portanto, este é um exemplo de algoritmo de Fibonacci muito rápido:
zipWith é uma função do Prelude padrão:
Teste:
Resultado:
Tempo decorrido: 0,00018s
fonte