Programação funcional
Estratégias de avaliação
Uma estratégia de avaliação determina quando uma expressão passada como argumento para uma função é avaliada.
Existem duas categorias principais de estratégias:
- Avaliação estrita (ou ansiosa, eager): os argumentos são avaliados completamente antes da aplicação da função
- Avaliação não-estrita (ou preguiçosa, lazy)
Considere o exemplo a seguir. Antes de executá-lo, tente adivinhar o que será escrito no console, na ordem correta.
function(str, info) { return simplesEval(str, info); }
Acertou? Chegou perto? Para entender o que aconteceu, temos que falar de estratégias de avaliação.
Conceitos
- Parâmetro formal: variável que aparece na definição de uma função
- Parâmetro real (ou argumento): variável ou valor passado em uma chamada para uma função
function soma(a, b) { // a e b são parâmetros formais
return a + b;
}
var x = 1, y = 2;
var z = soma(2*x, y); // 2*x e y são argumentos
// (parâmetros reais)
Avaliação estrita
Passagem por valor: o valor da expressão é avaliado antes da chamada da função; a função recebe uma cópia do valor.
Passagem por referência: a função recebe uma referência para a variável; a função pode modificar a variável e atribuir um novo valor a ela. Nesse caso o argumento não pode ser qualquer expressão; deve ser uma variável. Exemplo: função swap
em Pascal:
procedure swap(var x, y: integer);
var
temp: integer;
begin
temp := x;
x:= y;
y := temp;
end;
Não é possível escrever uma função similar em Javascript em usar artifícios, pois Javascript não dá suporte a passagem de parâmetros por referência.
Passagem por compartilhamento (às vezes chamado de passagem por referência): como na passagem por referência, a função consegue modificar o objeto passado, mas não consegue atribuir a variável a outro valor. Exemplo:
function(str, info) { return simplesEval(str, info); }
Podemos usar a passagem por compartilhamento para emular passagem por referência e escrever uma função swap
em Javascript:
function(str, info) { return simplesEval(str, info); }
Passagem por cópia e restauração (copy-restore). Como passagem por compartilhamento, útil no contexto de chamadas remotas (o chamador e a função estão em processos diferentes). O valor é copiado para a função, e ao final o novo valor é copiado de volta para o chamador.
Avaliação não-estrita
Chamada por nome. Nesta avaliação, os argumentos não são avaliados de maneira completa, tais argumentos são substituídos diretamente dentro do corpo da função. Se um parêmetro não é usado na avaliação da função, este nunca será avaliado, e se o parâmetro é usado varias vezes, este é reavaliado a cada vez.
Chamada por necessidade. Chamada por necessidade é uma versão da chamada por nome, se o argumento da função é avaliado, seu valor é gravado para uso posterior.
Chamada por expansão de macro. Ver #define
em C.
Lazy evaluation em Javascript
Embora Javascript não permita diretamente a passagem de parâmetros por necessidade, podemos escrever programas que usam essa estratégia.
Considere o seguinte exemplo, que tenta emular o operador ternário condicao ? valorVerdadeiro : valorFalso
:
function(str, info) { return simplesEval(str, info); }
Você deve ter percebido que é impossível emular perfeitamente o operador ternário, pois os argumentos são avaliados de forma estrita. O único jeito é reescrever ternario
para receber funções, e não valores.
function(str, info) { return simplesEval(str, info); }
Memoização
Consideremos outro exemplo:
function(str, info) { return simplesEval(str, info); }
Na primeira chamada a repete
, a expressão concatena(tranquilo, favoravel)
é avaliada apenas uma vez, e seu valor é passado ao parâmetro string
. Na segunda chamada, a expressão é avaliada, mas seu valor nunca chega a ser usado (por ele é não é impresso nenhuma vez). Isso representa um desperdício de processamento! Para resolver isso, vamos usar uma estratégia de avaliação preguiçosa:
function(str, info) { return simplesEval(str, info); }
Pronto! Agora, no segundo caso (nvezes
igual a 0
), a função concatena
não é chamada, porque não é necessário! Em compensação, a função é chamada 4 vezes no primeiro repete
, o que também é um desperdício, já que o resultado é sempre o mesmo. Essa é a desvantagem da passagem de parâmetros por nome. Vamos transformar em passagem por necessidade; para isso, é preciso guardar o valor na primeira chamada.
function(str, info) { return simplesEval(str, info); }
Essa técnica de guardar o retorno de uma função para consultar em chamadas subsequentes à função é chamada memoização.
Exercício: implementar chamadaMemoizada(f)
.
Aplicação prática: logging.
nivelAtual = 3;
function log(nivel, mensagem) {
if (nivel >= nivelAtual) {
console.log(mensagem);
}
}
if (nivelAtual >= 3) { // otimização para evitar computar a mensagem se não for ser exibida
log(3, "Usuário clicou em " + convertePosicao(mouse.x) + ", " + convertePosicao(mouse.y));
}
Listas preguiçosas com iterators
Um objeto é um iterador quando ele sabe como acessar itens de uma coleção um de cada vez, enquanto mantém o registro da posição atual dentro da sequência.
Em Javascript, um iterador é um objeto que fornece um método next()
que retorna o próximo item na sequência. Esse método retorna um objeto com duas propriedades: done
(indica se a sequência terminou) e value
(representa o item atual da sequência).
function(str, info) { return simplesEval(str, info); }
Vejamos outro exemplo, desta vez com um iterador infinito:
function(str, info) { return simplesEval(str, info); }
O conceito de gerador ou função geradora (generator function) permite escrever esse código de forma muito mais intuitiva. Para definir um gerador, use function*
. Para chamar um gerador, você pode usar o mesmo esquema de next()
, done
e value
ou pode usar for (x of gerador())
:
function(str, info) { return simplesEval(str, info); }
Podemos criar uma função genérica que executa uma função para os primeiros n
valores de um generator:
function(str, info) { return simplesEval(str, info); }