Pular para o conteúdo principal

4.1.6 Declarações Internas

Em JavaScript, o escopo de uma declaração é o bloco inteiro que imediatamente envolve a declaração, não apenas a porção do bloco começando no ponto onde a declaração ocorre. Esta seção examina mais de perto essa escolha de design.

Vamos revisitar o par de funções mutuamente recursivas is_even e is_odd da Seção 3.2.3, declaradas localmente no corpo de uma função f.

function f(x) {
function is_even(n) {
return n === 0
? true
: is_odd(n - 1);
}
function is_odd(n) {
return n === 0
? false
: is_even(n - 1);
}
return is_even(x);
}

Nossa intenção aqui é que o nome is_odd no corpo da função is_even deve se referir à função is_odd que é declarada após is_even. O escopo do nome is_odd é o bloco corpo inteiro de f, não apenas a porção do corpo de f começando no ponto onde a declaração de is_odd ocorre. De fato, quando consideramos que is_odd é ela mesma definida em termos de is_even—de modo que is_even e is_odd são funções mutuamente recursivas—vemos que a única interpretação satisfatória das duas declarações é considerá-las como se os nomes is_even e is_odd estivessem sendo adicionados ao ambiente simultaneamente. Mais geralmente, na estrutura de bloco, o escopo de um nome local é o bloco inteiro no qual a declaração é avaliada.

A avaliação de blocos no avaliador metacircular da seção 4.1.1 alcança tal escopo simultâneo para nomes locais escaneando as declarações no bloco e estendendo o ambiente atual com um frame contendo vinculações para todos os nomes declarados antes de avaliar as declarações. Assim, o novo ambiente no qual o corpo do bloco é avaliado já contém vinculações para is_even e is_odd, e qualquer ocorrência de um desses nomes refere-se à vinculação correta. Uma vez que suas declarações são avaliadas, esses nomes são vinculados aos seus valores declarados, ou seja, objetos de função que têm o ambiente estendido como sua parte de ambiente. Assim, por exemplo, quando is_even é aplicado no corpo de f, seu ambiente já contém a vinculação correta para o símbolo is_odd, e a avaliação do nome is_odd no corpo de is_even recupera o valor correto.

Exercício 4.16

Considere a função f_3 da seção 1.3.2:

function f_3(x, y) {
const a = 1 + x * y;
const b = 1 - y;
return x * square(a) + y * b + a * b;
}
  1. Desenhe um diagrama do ambiente em vigor durante a avaliação da expressão de retorno de f_3.

  2. Ao avaliar uma aplicação de função, o avaliador cria dois frames: um para os parâmetros e um para os nomes declarados diretamente no bloco corpo da função, em oposição a um bloco interno. Uma vez que todos esses nomes têm o mesmo escopo, uma implementação poderia combinar os dois frames. Altere o avaliador de modo que a avaliação do bloco corpo não crie um novo frame. Você pode assumir que isso não resultará em nomes duplicados no frame (o exercício 4.17 justifica isso).

💡 Mostrar solução — tente primeiro!💡 Esconder solução

1. O diagrama. A aplicação f_3(3, 4) (digamos) cria dois frames encadeados:

global (square, f_3, …)

E1: x = 3, y = 4 (frame dos parâmetros)

E2: a = 13, b = -3 (frame do bloco corpo)

A expressão de retorno é avaliada em E2, enxergando a e b ali e x, y em E1.

2. Combinar os frames. Em apply, em vez de avaliar o corpo como um bloco (que estende o ambiente com um frame novo), avalia-se a sequência interna do bloco diretamente no ambiente dos parâmetros, com os nomes locais varridos para o mesmo frame:

// no apply, para funções compostas:
const body = function_body(fun);
const locals = scan_out_declarations(body);
const values = list_of_unassigned(locals);
const program_env = extend_environment(
append(parameters(fun), locals),
append(args, values),
function_environment(fun));
evaluate_sequence(block_body_statements(body), program_env);

Um frame a menos por aplicação — válido porque parâmetros e locais diretos têm o mesmo escopo, e o exercício 4.17 garante que não há duplicatas.

Exercício 4.17

Eva Lu Ator está escrevendo programas nos quais declarações de função e outras declarações estão intercaladas. Ela precisa ter certeza de que as declarações são avaliadas antes que as funções sejam aplicadas. Ela reclama: "Por que o avaliador não pode cuidar dessa tarefa e içar todas as declarações de função para o início do bloco em que aparecem? Declarações de função fora de blocos devem ser içadas para o início do programa."

  1. Modifique o avaliador seguindo a sugestão de Eva.

  2. Os designers do JavaScript decidiram seguir a abordagem de Eva. Discuta esta decisão.

  3. Além disso, os designers do JavaScript decidiram permitir que o nome declarado por uma declaração de função seja reatribuído usando atribuição. Modifique sua solução de acordo e discuta esta decisão.

💡 Mostrar solução — tente primeiro!💡 Esconder solução

1. Içar declarações de função. Na avaliação de um bloco, além de varrer os nomes declarados (que já viram "*unassigned*"), o avaliador percorre a sequência, separa as declarações de função e as avalia primeiro, na ordem em que aparecem, antes das demais declarações:

function eval_block(component, env) {
const body = block_body(component);
const locals = scan_out_declarations(body);
const new_env = extend_environment(
locals, list_of_unassigned(locals), env);
const fun_decls = filter(is_function_declaration, body_statements(body));
for_each(decl => evaluate(decl, new_env), fun_decls);
return evaluate_sequence(
filter(s => ! is_function_declaration(s), body_statements(body)),
new_env);
}

2. A decisão. O hoisting deixa Eva chamar funções antes do ponto do texto em que foram declaradas — conveniente para organizar programas com as definições auxiliares no fim, e essencial para funções mutuamente recursivas intercaladas com outras declarações. O custo é que o programa deixa de poder ser lido de cima para baixo como uma sequência: a ordem textual e a ordem de avaliação divergem, e um leitor pode se surpreender ("de onde veio esse nome?"). É uma troca deliberada de simplicidade semântica por ergonomia.

3. Permitir reatribuição. Basta que a varredura registre nomes de declaração de função como variáveis (como let), não como constantes — atribuições posteriores encontram um vínculo mutável. A decisão é discutível: torna possível o idioma de sobrescrever uma função (útil para instrumentação, como o trace deste site!), mas abre a porta para um nome de função significar coisas diferentes em momentos diferentes — mais uma dose do veneno da atribuição do capítulo 3, agora aplicada às próprias funções.

Exercício 4.18

Funções recursivas são obtidas de uma maneira indireta em nosso interpretador: Primeiro declare o nome que se referirá à função recursiva e atribua a ele o valor especial "*unassigned*"; depois defina a função recursiva no escopo desse nome; e finalmente atribua a função definida ao nome. Quando a função recursiva é aplicada, quaisquer ocorrências do nome no corpo se referem apropriadamente à função recursiva. Surpreendentemente, é possível especificar funções recursivas sem usar declarações ou atribuição. O programa a seguir calcula 10 fatorial aplicando uma função fatorial recursiva:1

💡 Mostrar solução — tente primeiro!💡 Esconder solução

O programa aplica o truque de passar a função para si mesma:

(n => (fact => fact(fact, n))
((ft, k) => k === 1 ? 1 : k * ft(ft, k - 1)))
(10);

Como funciona, de dentro para fora: a lambda (ft, k) => … não se refere a si mesma por nome — recebe a si própria como o argumento ft, e a chamada recursiva é ft(ft, k - 1): passa a função adiante de novo. A lambda fact => fact(fact, n) dá o pontapé, aplicando a função a ela mesma. Nenhum nome é declarado, nenhuma atribuição acontece — a recursão emerge só de aplicação de funções.

Experimente (funciona em JavaScript de verdade!):

Exemplo de Código
Código editável · Ctrl+Enter executa · Esc solta o foco

É o embrião do operador Y do cálculo lambda: o padrão f(f) interno é o que os teóricos chamam de auto-aplicação, e generalizá-lo dá recursão para qualquer função sem suporte da linguagem.

(n => (fact => fact(fact, n))
((ft, k) => k === 1
? 1
: k * ft(ft, k - 1)))(10);
  1. Verifique (avaliando a expressão) que isso realmente calcula fatoriais. Elabore uma expressão análoga para calcular números de Fibonacci.

  2. Considere a função f dada acima:

function f(x) {
function is_even(n) {
return n === 0
? true
: is_odd(n - 1);
}
function is_odd(n) {
return n === 0
? false
: is_even(n - 1);
}
return is_even(x);
}

Preencha as expressões faltantes para completar uma declaração alternativa de f, que não tem declarações internas de função:

function f(x) {
return ((is_even, is_odd) => is_even(is_even, is_odd, x))
((is_ev, is_od, n) => n === 0 ? true : is_od(??, ??, ??),
(is_ev, is_od, n) => n === 0 ? false : is_ev(??, ??, ??));
}

📝 Encontrou algo errado nesta página?

Sua ajuda é muito importante para melhorar a qualidade da tradução!

🐛 Encontrou um erro?

Se você encontrou:

  • Erro de tradução (palavra incorreta, termo técnico errado)
  • Erro de ortografia ou gramática
  • Link quebrado
  • Código de exemplo que não funciona
  • Problema de formatação

Reporte um bug →

❓ Tem uma dúvida?

Se você tem:

  • Dúvida sobre o conteúdo desta seção
  • Pergunta sobre um conceito do SICP
  • Dificuldade em entender algum exemplo
  • Questão sobre a tradução de algum termo

Inicie uma discussão →

💡 Tem uma sugestão de melhoria?

Se você quer sugerir:

  • Melhoria na explicação
  • Exemplo adicional
  • Recurso visual (diagrama, ilustração)
  • Qualquer outra ideia

Sugira uma melhoria →

🌍 Quer discutir a tradução?

Se você quer debater:

  • Escolha de tradução de algum termo
  • Consistência de terminologia
  • Nuances do português

Discussão de tradução →

Obrigado por ajudar a melhorar o SICP.js PT-BR! ✨

Footnotes

  1. Este exemplo ilustra um truque de programação para formular funções recursivas sem usar atribuição. O truque mais geral deste tipo é o operador Y, que pode ser usado para dar uma implementação de "cálculo lambda puro" de recursão. (Veja Stoy 1977 para detalhes sobre o cálculo lambda, e Gabriel 1988 para uma exposição do operador Y na linguagem Scheme.)