Generators
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.
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:
Podemos usar a passagem por compartilhamento para emular passagem por referência e escrever uma função swap
em Javascript:
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
:
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.
Memoização
Consideremos outro exemplo:
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:
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.
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).
Vejamos outro exemplo, desta vez com um iterador infinito:
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())
:
Podemos criar uma função genérica que executa uma função para os primeiros n
valores de um generator:
Exercícios
Considere a seguinte função, que imprime os primeiros n
números primos:
Essa função tem várias responsabilidades:
- determinar se um número é primo
- controlar a quantidade de números retornados
- imprimir números
Do jeito que está, a função não é muito extensível. E se quiséssemos retornar os números primos em vez de imprimi-los? E se quiséssemos obter os números primos menores que 100? Qualquer dessas mudanças exigiria a implementação de uma nova função.
Ainda bem que Javascript é uma linguagem funcional! Vamos aproveitar os recursos da linguagem para decompor essa função em funções menores, cada uma com uma única responsabilidade, para facilitar o reuso das funções?
Isolando a checagem de números primos
Vamos decompor a função original em duas partes: a função ehPrimo(numero)
determina se um número é primo ou não, e imprimePrimeiros(n, pred)
imprime os primeiros n
números naturais que satisfazem à função pred
. Com isso, podemos reescrever imprimePrimeirosPrimos(n)
combinando as duas funções.
Isolando a forma de retorno
A função atual imprime o resultado no console. E se quiséssemos mostrar em caixas de alerta? E se quiséssemos retornar um array? Vamos reescrever a função para dar essa possibilidade.
Transforme a função primeirosPred em um generator.
Nesse caso, você não precisará mais passar a função func
, uma vez que primeirosPred
retornará diretamente o valor através da instrução yield
.