GHC não memoriza funções.
No entanto, ele calcula qualquer expressão dada no código no máximo uma vez por vez que a expressão lambda circundante é inserida, ou no máximo uma vez se estiver no nível superior. Determinar onde estão as expressões lambda pode ser um pouco complicado quando você usa açúcar sintático como em seu exemplo, então vamos convertê-los em sintaxe desugared equivalente:
m1' = (!!) (filter odd [1..]) -- NB: See below!
m2' = \n -> (!!) (filter odd [1..]) n
(Nota: O relatório Haskell 98 realmente descreve uma seção esquerda do operador como (a %)
como equivalente a \b -> (%) a b
, mas o GHC a desugou (%) a
. Eles são tecnicamente diferentes porque podem ser diferenciados por seq
. Acho que devo ter enviado um tíquete do GHC Trac sobre isso.)
Dado isso, você pode ver que em m1'
, a expressão filter odd [1..]
não está contida em nenhuma expressão lambda, então ela só será calculada uma vez por execução de seu programa, enquanto emm2'
, filter odd [1..]
será computado cada vez que a expressão lambda for inserida, ou seja, em cada chamada de m2'
. Isso explica a diferença de tempo que você está vendo.
Na verdade, algumas versões do GHC, com certas opções de otimização, irão compartilhar mais valores do que a descrição acima indica. Isso pode ser problemático em algumas situações. Por exemplo, considere a função
f = \x -> let y = [1..30000000] in foldl' (+) 0 (y ++ [x])
GHC pode perceber que y
não depende dex
e reescrever a função para
f = let y = [1..30000000] in \x -> foldl' (+) 0 (y ++ [x])
Neste caso, a nova versão é muito menos eficiente porque terá que ler cerca de 1 GB da memória onde y
está armazenada, enquanto a versão original rodaria em espaço constante e caberia no cache do processador. Na verdade, no GHC 6.12.1, a função f
é quase duas vezes mais rápida quando compilada sem otimizações do que com a qual é compilada -O2
.
seq
m1 10000000). No entanto, há uma diferença quando nenhum sinalizador de otimização é especificado. E ambas as variantes do seu "f" têm residência máxima de 5356 bytes, independentemente da otimização, a propósito (com menos alocação total quando -O2 é usado).f
:main = interact $ unlines . (show . map f . read) . lines
; compilar com ou sem-O2
; entãoecho 1 | ./main
. Se você escrever um teste comomain = print (f 5)
, então oy
lixo pode ser coletado conforme é usado e não há diferença entre os doisf
s.map (show . f . read)
, é claro. E agora que baixei o GHC 6.12.3, vejo os mesmos resultados do GHC 6.12.1. E sim, você está certo sobre o originalm1
em2
: as versões do GHC que realizam esse tipo de levantamento com otimizações habilitadas se transformarãom2
emm1
.m1 é calculado apenas uma vez porque é um formulário de aplicação constante, enquanto m2 não é um CAF e, portanto, é calculado para cada avaliação.
Veja o wiki do GHC em CAFs: http://www.haskell.org/haskellwiki/Constant_applicative_form
fonte
[1 ..]
é calculada apenas uma vez durante a execução de um programa ou é calculada uma vez por aplicação da função, mas ela está relacionada ao CAF?m1
é um CAF, o segundo se aplica efilter odd [1..]
(não apenas[1..]
!) É calculado apenas uma vez. O GHC também pode observar quem2
se refere afilter odd [1..]
, e colocar um link para o mesmo thunk usado nom1
, mas isso seria uma má ideia: poderia levar a grandes vazamentos de memória em algumas situações.[1..]
efilter odd [1..]
. Quanto ao resto, ainda não estou convencido. Se não me engano, CAF só é relevante quando queremos argumentar que um compilador poderia substituir ofilter odd [1..]
inm2
por um thunk global (que pode ser até o mesmo thunk que o usado emm1
). Mas na situação do solicitante, o compilador não fez essa “otimização” e não consigo ver sua relevância para a questão.m1
, e ele faz.Há uma diferença crucial entre as duas formas: a restrição de monomorfismo se aplica a m1, mas não a m2, porque m2 deu argumentos explicitamente. Então o tipo de m2 é geral, mas o tipo m1 é específico. Os tipos a que são atribuídos são:
A maioria dos compiladores e interpretadores Haskell (todos eles que eu conheço) não memorizam estruturas polimórficas, então a lista interna de m2 é recriada toda vez que é chamada, enquanto a de m1 não é.
fonte
Não tenho certeza, porque sou bastante novo no Haskell, mas parece que é porque a segunda função está parametrizada e a primeira não. A natureza da função é que, seu resultado depende do valor de entrada e no paradigma funcional especialmente depende SOMENTE da entrada. A implicação óbvia é que uma função sem parâmetros retorna sempre o mesmo valor indefinidamente, não importa o quê.
Aparentemente, existe um mecanismo de otimização no compilador GHC que explora esse fato para calcular o valor de tal função apenas uma vez para o tempo de execução do programa inteiro. Ele faz isso preguiçosamente, com certeza, mas o faz mesmo assim. Eu mesmo percebi, quando escrevi a seguinte função:
Em seguida, testá-lo, entrei GHCI e escreveu:
primes !! 1000
. Demorou alguns segundos, mas finalmente eu tenho a resposta:7927
. Então ligueiprimes !! 1001
e obtive a resposta instantaneamente. Da mesma forma, em um instante obtive o resultado paratake 1000 primes
, porque Haskell teve que calcular toda a lista de mil elementos para retornar o 1001º elemento antes.Portanto, se você pode escrever sua função de forma que ela não receba parâmetros, provavelmente você a deseja. ;)
fonte