Uma função pura memorizada é considerada pura?

47

Digamos que fn(x)é uma função pura que faz algo caro, como retornar uma lista dos principais fatores de x.

E digamos que criamos uma versão memorizada da mesma função chamada memoizedFn(x). Ele sempre retorna o mesmo resultado para uma determinada entrada, mas mantém um cache privado dos resultados anteriores para melhorar o desempenho.

Formalmente falando, é memoizedFn(x)considerado puro?

Ou existe algum outro nome ou termo qualificado usado para se referir a essa função nas discussões sobre PF? (ou seja, uma função com efeitos colaterais que podem afetar a complexidade computacional das chamadas subseqüentes, mas que não afetam os valores de retorno.)

callum
fonte
24
Talvez não seja puro para puristas, mas "puro o suficiente" para pessoas pragmáticas ;-)
Doc Brown
2
@DocBrown Eu concordo, me perguntando se existe um termo mais formal para 'puro o suficiente'
callum
13
A execução de uma função pura provavelmente modificará o cache de instruções do processador, os preditores de ramificação etc. Mas isso provavelmente é "suficientemente puro" para os puristas também - ou você pode esquecer completamente as funções puras.
gnasher729 26/08
10
@ callum Não, não existe uma definição formal de "puro o suficiente". Ao discutir sobre pureza e a equivalência semântica de duas chamadas "referencialmente transparentes", você sempre precisa indicar exatamente qual semântica será aplicada. Em algum nível baixo de detalhes da implementação, ele sempre será interrompido e terá diferentes efeitos ou tempos de memória. É por isso que você precisa ser pragmático: que nível de detalhe é útil para raciocinar sobre seu código?
Bergi
3
Então, por uma questão de pragmatismo, eu diria que a pureza depende se você considera ou não o tempo de computação como parte da saída. funcx(){sleep(cached_time--); return 0;}retorna o mesmo val todas as vezes, mas terá um desempenho diferente
Mars

Respostas:

41

Sim. A versão memorizada de uma função pura também é uma função pura.

Tudo o que se preocupa com a pureza da função é o efeito que os parâmetros de entrada no valor de retorno da função (passar a mesma entrada sempre deve produzir a mesma saída) e quaisquer efeitos colaterais relevantes para os estados globais (por exemplo, texto para o terminal, interface do usuário ou rede) . O tempo de computação e o uso extra de memória são irrelevantes para a pureza da função.

Os caches de uma função pura são praticamente invisíveis para o programa; é permitido que uma linguagem de programação funcional otimize automaticamente uma função pura para uma versão memorizada da função, se puder determinar que será benéfico fazê-lo. Na prática, determinar automaticamente quando a memorização é benéfica é realmente um problema bastante difícil, mas essa otimização seria válida.

Lie Ryan
fonte
19

A Wikipedia define uma "Função pura" como uma função que possui as seguintes propriedades:

  • Seu valor de retorno é o mesmo para os mesmos argumentos (nenhuma variação com variáveis ​​estáticas locais, variáveis ​​não locais, argumentos de referência mutáveis ​​ou fluxos de entrada de dispositivos de E / S).

  • Sua avaliação não tem efeitos colaterais (nenhuma mutação de variáveis ​​estáticas locais, variáveis ​​não locais, argumentos de referência mutáveis ​​ou fluxos de E / S).

Com efeito, uma função pura retorna a mesma saída, dada a mesma entrada, e não afeta mais nada fora da função. Para fins de pureza, não importa como a função calcula seu valor de retorno, desde que retorne a mesma saída, com a mesma entrada.

Linguagens funcionalmente puras, como Haskell, usam rotineiramente a memorização para acelerar uma função, armazenando em cache seus resultados calculados anteriormente.

Robert Harvey
fonte
16
Eu posso sentir falta de alguma coisa, mas como você vai manter o cache sem efeitos colaterais?
val
1
Mantendo-o dentro da função.
Robert Harvey
4
"nenhuma mutação da variável estática local" parece excluir variáveis ​​locais persistentes entre as chamadas também.
val
3
Na verdade, isso não responde à pergunta, mesmo que você pareça implicar que sim, é pura.
Marte
6
@val Você está correto: essa condição precisa ser relaxada um pouco. A memorização puramente funcional a que ele se refere não tem mutação visível de nenhum dado estático. O que acontece é que o resultado é calculado e memorizado na primeira vez que a função é chamada e retorna o mesmo valor sempre que é chamado. Muitos idiomas têm um idioma para isso: uma static constvariável local em C ++ (mas não C) ou uma estrutura de dados avaliada preguiçosamente em Haskell. Você precisa de mais uma condição: a inicialização deve ser segura para threads.
Davislor 27/08
7

Sim, funções puras memorizadas são comumente referidas como puras. Isso é especialmente comum em idiomas como Haskell, nos quais resultados imutáveis, memorizados, com preguiça de avaliar preguiçosamente são um recurso interno.

Há uma ressalva importante: a função de memorização deve ser segura para threads, ou você pode ter uma condição de corrida quando dois threads tentam chamá-lo.

Um exemplo de cientista da computação que usa o termo "puramente funcional" dessa maneira é este post de Conal Elliott sobre memoização automática:

Talvez surpreendentemente, a memorização possa ser implementada de maneira simples e puramente funcional em uma linguagem funcional preguiçosa.

Existem muitos exemplos na literatura revisada por pares e existem há décadas. Por exemplo, este artigo de 1995, “Usando a memorização automática como ferramenta de engenharia de software em sistemas de IA do mundo real”, usa linguagem muito semelhante na seção 5.2 para descrever o que hoje chamaríamos de função pura:

A memorização funciona apenas para funções verdadeiras, não para procedimentos. Ou seja, se o resultado de uma função não for especificado de forma completa e determinística por seus parâmetros de entrada, o uso da memorização fornecerá resultados incorretos. O número de funções que podem ser memorizadas com sucesso será aumentado, incentivando o uso de um estilo de programação funcional em todo o sistema.

Algumas linguagens imperativas têm um idioma semelhante. Por exemplo, uma static constvariável em C ++ é inicializada apenas uma vez, antes que seu valor seja usado e nunca sofre mutação.

Davislor
fonte
3

Depende de como você faz.

Geralmente, as pessoas querem memorizar modificando algum tipo de dicionário de cache. Isso tem todos os problemas associados à mutação impura, como ter que se preocupar com simultaneidade, se preocupar com o cache ficar muito grande etc.

No entanto, você pode memorizar sem mutação impura na memória. Um exemplo é nesta resposta , onde eu rastreio os valores memorizados externamente por meio de um lengthsargumento.

No link fornecido por Robert Harvey , a avaliação preguiçosa é usada para evitar efeitos colaterais.

Outra técnica às vezes vista é marcar explicitamente a memoização como um efeito colateral impuro no contexto de um IOtipo, como na função memoize do efeito de gato .

Este último traz o argumento de que, às vezes, o objetivo é apenas encapsular a mutação em vez de eliminá-la. A maioria dos programadores funcionais considera "puro o suficiente" para tornar a impureza explícita e encapsulada.

Se você quer que um termo o diferencie de uma função verdadeiramente pura, acho que basta dizer "memorizado com um dicionário mutável". Isso permite que as pessoas saibam usá-lo com segurança.

Karl Bielefeldt
fonte
Acho que nenhuma solução mais pura resolve os problemas acima: Embora você perca qualquer preocupação com a concorrência, também perde a chance de duas chamadas iniciadas simultaneamente como collatz(100)e collatz(200)para cooperar. E IIUIC, o problema com o cache cresce muito permanece (embora Haskell possa ter alguns truques interessantes para isso?).
maaartinus
Nota: IOé puro. Todos os métodos impuros IOe Gatos são nomeados unsafe. Async.memoizetambém é puro, por isso não precisamos nos contentar com "puro o suficiente" :)
Samuel
2

Geralmente, uma função que retorna uma lista não é pura, porque requer alocação de armazenamento e pode falhar (por exemplo, lançando uma exceção que não é pura). Um idioma que possui tipos de valor e pode representar uma lista como um tipo de valor de tamanho limitado pode não ter esse problema. Por esse motivo, seu exemplo provavelmente não é puro.

Em geral, se a memorização puder ser feita de maneira isenta de falhas (por exemplo, tendo armazenamento estaticamente alocado para resultados memorizados e sincronização interna para controlar o acesso a eles se o idioma admitir encadeamentos), é razoável considerar essa função puro.

R ..
fonte
0

Você pode implementar a memorização sem efeitos colaterais usando a mônada do estado .

[State monad] é basicamente uma função S => (S, A), onde S é o tipo que representa seu estado e A é o resultado que a função produz - Cats State .

No seu caso, o estado seria o valor memorizado ou nada (ou seja, Haskell Maybeou Scala Option[A]). Se o valor memorizado estiver presente, ele será retornado como A, caso contrário, Aserá calculado e retornado como o estado de transição e o resultado.

Samuel
fonte