Nome: ? | Matrícula: ?

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:

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

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); }