Pular para o conteúdo principal

3.5.2 Streams Infinitos

Vimos como suportar a ilusão de manipular streams como entidades completas, mesmo que, na realidade, computemos apenas tanto do stream quanto precisamos acessar. Podemos explorar essa técnica para representar sequências eficientemente como streams, mesmo se as sequências forem muito longas. O que é mais impressionante, podemos usar streams para representar sequências infinitamente longas. Por exemplo, considere a seguinte definição do stream de inteiros positivos:

function integers_starting_from(n) {
return pair(n, () => integers_starting_from(n + 1));
}

const integers = integers_starting_from(1);

Isso faz sentido porque integers será um par cujo head é 1 e cujo tail é uma promessa de produzir os inteiros começando com 2. Este é um stream infinitamente longo, mas em qualquer momento dado podemos examinar apenas uma porção finita dele. Assim, nossos programas nunca saberão que o stream infinito inteiro não está lá.

Usando integers podemos definir outros streams infinitos, como o stream de inteiros que não são divisíveis por 7:

function is_divisible(x, y) { return x % y === 0; }

const no_sevens = stream_filter(x => ! is_divisible(x, 7),
integers);

Então podemos encontrar inteiros não divisíveis por 7 simplesmente acessando elementos deste stream:

stream_ref(no_sevens, 100);
117

Por analogia com integers, podemos definir o stream infinito de números de Fibonacci:

function fibgen(a, b) {
return pair(a, () => fibgen(b, a + b));
}

const fibs = fibgen(0, 1);

A constante fibs é um par cujo head é 0 e cujo tail é uma promessa de avaliar fibgen(1, 1). Quando avaliamos este fibgen(1, 1) atrasado, ele produzirá um par cujo head é 1 e cujo tail é uma promessa de avaliar fibgen(1, 2), e assim por diante.

Para ver um stream infinito mais emocionante, podemos generalizar o exemplo de no_sevens para construir o stream infinito de números primos, usando um método conhecido como o crivo de Eratóstenes.1 Começamos com os inteiros começando com 2, que é o primeiro primo. Para obter o resto dos primos, começamos filtrando os múltiplos de 2 do resto dos inteiros. Isso deixa um stream começando com 3, que é o próximo primo. Agora filtramos os múltiplos de 3 do resto deste stream. Isso deixa um stream começando com 5, que é o próximo primo, e assim por diante. Em outras palavras, construímos os primos por um processo de peneiramento, descrito da seguinte forma: Para peneirar um stream S, forme um stream cujo primeiro elemento é o primeiro elemento de S e o resto do qual é obtido filtrando todos os múltiplos do primeiro elemento de S do resto de S e peneirando o resultado. Este processo é prontamente descrito em termos de operações de stream:

function sieve(stream) {
return pair(head(stream),
() => sieve(stream_filter(
x => ! is_divisible(x, head(stream)),
stream_tail(stream))));
}
const primes = sieve(integers_starting_from(2));

Agora para encontrar um primo particular precisamos apenas pedi-lo:

stream_ref(primes, 50);
233

É interessante contemplar o sistema de processamento de sinais configurado por sieve, mostrado no "diagrama de Henderson" na figura 3.31. O stream de entrada alimenta um "desempacotador" que separa o primeiro elemento do stream do resto do stream. O primeiro elemento é usado para construir um filtro de divisibilidade, através do qual o resto é passado, e a saída do filtro é alimentada para outra caixa de peneiramento. Então o primeiro elemento original é adjunto à saída do peneiramento interno para formar o stream de saída. Assim, não apenas o stream é infinito, mas o processador de sinal também é infinito, porque o crivo contém um crivo dentro dele.

Definindo streams implicitamente

Os streams integers e fibs acima foram definidos especificando funções "geradoras" que explicitamente computam os elementos do stream um por um. Uma maneira alternativa de especificar streams é tirar vantagem da avaliação atrasada para definir streams implicitamente. Por exemplo, a seguinte instrução define o stream ones para ser um stream infinito de uns:

const ones = pair(1, () => ones);

Isso funciona muito como a declaração de uma função recursiva: ones é um par cujo head é 1 e cujo tail é uma promessa de avaliar ones. Avaliar o tail nos dá novamente um 1 e uma promessa de avaliar ones, e assim por diante.

Podemos fazer coisas mais interessantes manipulando streams com operações como add_streams, que produz a soma elemento por elemento de dois streams dados:2

function add_streams(s1, s2) {
return stream_map_2((x1, x2) => x1 + x2, s1, s2);
}

Agora podemos definir os inteiros da seguinte forma:

const integers = pair(1, () => add_streams(ones, integers));

Isso define integers para ser um stream cujo primeiro elemento é 1 e o resto do qual é a soma de ones e integers. Assim, o segundo elemento de integers é 1 mais o primeiro elemento de integers, ou 2; o terceiro elemento de integers é 1 mais o segundo elemento de integers, ou 3; e assim por diante. Esta definição funciona porque, em qualquer ponto, o suficiente do stream integers foi gerado de modo que podemos alimentá-lo de volta à definição para produzir o próximo inteiro.

Podemos definir os números de Fibonacci no mesmo estilo:

const fibs = pair(0,
() => pair(1,
() => add_streams(stream_tail(fibs),
fibs)));

Esta definição diz que fibs é um stream começando com 0 e 1, tal que o resto do stream pode ser gerado adicionando fibs a si mesmo deslocado por um lugar:

1 1 2 3 5 8 13 21 ... = stream_tail(fibs)
0 1 1 2 3 5 8 13 ... = fibs
-------------------------
0 1 1 2 3 5 8 13 21 34 ... = fibs

A função scale_stream também é útil na formulação de tais definições de stream. Isso multiplica cada item em um stream por uma constante dada:

function scale_stream(stream, factor) {
return stream_map(x => x * factor,
stream);
}

Por exemplo,

const double = pair(1, () => scale_stream(double, 2));

produz o stream de potências de 2: 1, 2, 4, 8, 16, 32, ...

Uma definição alternativa do stream de primos pode ser dada começando com os inteiros e filtrando-os testando para primalidade. Precisaremos do primeiro primo, 2, para começar:

const primes = pair(2,
() => stream_filter(is_prime,
integers_starting_from(3)));

Esta definição não é tão direta quanto parece, porque testaremos se um número nn é primo verificando se nn é divisível por um primo (não por qualquer inteiro) menor ou igual a n\sqrt{n}:

function is_prime(n) {
function iter(ps) {
return square(head(ps)) > n
? true
: is_divisible(n, head(ps))
? false
: iter(stream_tail(ps));
}
return iter(primes);
}

Esta é uma definição recursiva, já que primes é definido em termos do predicado is_prime, que por si só usa o stream primes. A razão desta função funcionar é que, em qualquer ponto, o suficiente do stream primes foi gerado para testar a primalidade dos números que precisamos verificar em seguida. Ou seja, para todo nn que testamos para primalidade, ou nn não é primo (caso em que há um primo já gerado que o divide) ou nn é primo (caso em que há um primo já gerado—isto é, um primo menor que nn—que é maior que n\sqrt{n}).

Exercício 3.53

Sem executar o programa, descreva os elementos do stream definido por

const s = pair(1, () => add_streams(s, s));
💡 Mostrar solução — tente primeiro!💡 Esconder solução

Sem executar: a cabeça é 1, e cada elemento seguinte é a soma do stream consigo mesmo — ou seja, o dobro do anterior. O stream é o das potências de 2: 1, 2, 4, 8, 16, …

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

Exercício 3.54

Defina uma função mul_streams, análoga a add_streams, que produz o produto elemento por elemento de seus dois streams de argumento de entrada. Use isso juntamente com o stream integers para completar a seguinte definição do stream cujos elementos são os fatoriais:

const factorials = pair(1, () => mul_streams(..., ...));
💡 Mostrar solução — tente primeiro!💡 Esconder solução
Exemplo de Código
Código editável · Ctrl+Enter executa · Esc solta o foco

Por que funciona. O nn-ésimo fatorial é o (n1)(n-1)-ésimo vezes n+1n+1… melhor dizendo: factorials[k] = factorials[k-1] * (k+1), e é exatamente isso que multiplicar factorials pelos inteiros a partir de 2, elemento a elemento, produz: 1, 2, 6, 24, 120, …

Exercício 3.55

Defina uma função partial_sums que recebe como argumento um stream SS e retorna o stream cujos elementos são S0S_0, S0+S1S_0 + S_1, S0+S1+S2S_0 + S_1 + S_2, ... Por exemplo, partial_sums(integers) deve ser o stream 1, 3, 6, 10, 15, ...

💡 Mostrar solução — tente primeiro!💡 Esconder solução
Exemplo de Código
Código editável · Ctrl+Enter executa · Esc solta o foco

Por que funciona. O kk-ésimo elemento de partial_sums(S) é o (k1)(k-1)-ésimo mais SkS_k — somar stream_tail(s) com o próprio partial_sums(s) desloca e acumula. O memo evita que partial_sums(s) seja reconstruído a cada elemento (sem ele o custo vira quadrático).

Exercício 3.56

Um famoso problema, apresentado pela primeira vez por R. Hamming, é enumerar, em ordem ascendente sem repetições, todos os inteiros positivos sem fatores primos além de 2, 3 ou 5. Uma maneira óbvia de fazer isso é simplesmente testar cada inteiro por sua vez para ver se tem outros fatores primos além de 2, 3 e 5. Mas isso é muito ineficiente, já que, à medida que os inteiros ficam maiores, cada vez menos deles atenderão ao requisito. Como alternativa, vamos chamar o stream requerido de S e notar os seguintes fatos sobre ele:

  • S começa com 1.
  • Os elementos de scale_stream(S, 2) também são elementos de S.
  • O mesmo é verdade para scale_stream(S, 3) e scale_stream(S, 5).
  • Estes são todos os elementos de S.

Agora tudo o que temos a fazer é combinar elementos dos três streams. Para isso precisamos definir uma função merge que combina dois streams ordenados em um stream ordenado resultado, eliminando repetições. Então o stream requerido pode ser construído com

const S = pair(1, () => merge(..., merge(..., ...)));

Complete a definição de S preenchendo as partes faltantes na definição da função merge e fornecendo as expressões faltantes nas lacunas.

💡 Mostrar solução — tente primeiro!💡 Esconder solução
Exemplo de Código
Código editável · Ctrl+Enter executa · Esc solta o foco

O caso de igualdade avança os dois streams — é o que elimina as repetições (12 = 2·6 = 3·4 aparece uma vez só). Os primeiros quinze: 1, 2, 3, 4, 5, 6, 8, 9, 10, 12, 15, 16, 18, 20, 24.

Exercício 3.57

Quantas adições são realizadas quando computamos o nn-ésimo número de Fibonacci usando a definição de fibs baseada em add_streams? Mostre que o número de adições seria exponencialmente maior se tivéssemos implementado add_streams sem usar a otimização de memoização fornecida pela função memo (exercício 3.50).

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

Com memoização, o nn-ésimo Fibonacci custa n1n - 1 adições: cada elemento do stream é computado uma única vez, e computar o elemento kk faz exatamente uma adição sobre dois elementos já lembrados.

Sem memoização, cada acesso à cauda reconstrói os dois streams operandos do add_streams. O número de adições A(n)A(n) para materializar o elemento nn satisfaz

A(n)=A(n1)+A(n2)+1A(n) = A(n-1) + A(n-2) + 1

— a mesma recorrência de fib em árvore do capítulo 1. Como A(n)+1=(A(n1)+1)+(A(n2)+1)A(n) + 1 = (A(n-1) + 1) + (A(n-2) + 1), temos A(n)+1Fib(n+1)A(n) + 1 \ge \text{Fib}(n+1), que cresce como φn\varphi^n: exponencial. A memoização é o que separa a definição elegante de uma definição utilizável.

Exercício 3.58

Dê uma interpretação da função stream

function expand(num, den, radix) {
return pair(math_trunc((num * radix) / den),
() => expand((num * radix) % den, den, radix));
}

(math_trunc produz a parte inteira de seu argumento.) O que são os streams sucessivos gerados por

expand(1, 7, 10);
expand(3, 8, 10);
💡 Mostrar solução — tente primeiro!💡 Esconder solução

expand(num, den, radix) produz os dígitos da expansão de num/den na base radix, depois da vírgula: cada passo multiplica o resto por radix, tira o dígito inteiro e continua com o novo resto — o algoritmo escolar de divisão contínua.

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

expand(1, 7, 10)1 4 2 8 5 7 1 4 … — o período de 1/7=0,1428571/7 = 0{,}\overline{142857}; expand(3, 8, 10)3 7 5 0 0 0 … — pois 3/8=0,3753/8 = 0{,}375.

Exercício 3.59

Na seção 2.5.3 vimos como implementar uma aritmética de polinômios representando polinômios como listas de coeficientes. Aqui trabalharemos com séries de potências infinitas, que também podem ser representadas como streams infinitos. Representaremos a série de potências a0+a1x+a2x2+a3x3+a_0 + a_1 x + a_2 x^2 + a_3 x^3 + \cdots como o stream de coeficientes (a0,a1,a2,a3,)(a_0, a_1, a_2, a_3, \ldots).

a. A série de potências integral pode ser realizada termo por termo:

(a0+a1x+a2x2+a3x3+)dx=C+a0x+12a1x2+13a2x3+14a3x4+\int(a_0 + a_1 x + a_2 x^2 + a_3 x^3 + \cdots)dx = C + a_0 x + \frac{1}{2} a_1 x^2 + \frac{1}{3} a_2 x^3 + \frac{1}{4} a_3 x^4 + \cdots

(onde CC é qualquer constante de integração). Defina uma função integrate_series que recebe como entrada um stream a0,a1,a2,a_0, a_1, a_2, \ldots representando uma série de potências e retorna o stream a0,12a1,13a2,a_0, \frac{1}{2} a_1, \frac{1}{3} a_2, \ldots de coeficientes da série de potências integral não integrada (ou seja, a série para a integral da série original, assumindo constante zero de integração).

b. A função exponencial (assim como seno e cosseno) pode ser definida em termos da integração de sua própria derivada:

dexdx=ex\frac{de^x}{dx} = e^x

com e0=1e^0 = 1. Gere o stream de séries de potências para exe^x.

💡 Mostrar solução — tente primeiro!💡 Esconder solução
Exemplo de Código
Código editável · Ctrl+Enter executa · Esc solta o foco

Os coeficientes conferem com as séries de Taylor: exe^x: 1,1,12,16,1241, 1, \tfrac12, \tfrac16, \tfrac1{24}; cosseno: 1,0,12,0,124,01, 0, -\tfrac12, 0, \tfrac1{24}, 0; seno: 0,1,0,16,0,11200, 1, 0, -\tfrac16, 0, \tfrac1{120} — as definições mutuamente recursivas funcionam porque a cabeça de cada série está disponível antes de a cauda ser exigida.

Exercício 3.60

Com séries de potências representadas como streams de coeficientes como na série do exercício 3.59, adicionar séries é implementado por add_streams. Complete a definição da seguinte função para multiplicar séries:

function mul_series(s1, s2) {
pair(...,
() => add_streams(..., ...));
}

Você pode testar sua função verificando que sin2x+cos2x=1\sin^2 x + \cos^2 x = 1, usando as séries do exercício 3.59.

💡 Mostrar solução — tente primeiro!💡 Esconder solução
Exemplo de Código
Código editável · Ctrl+Enter executa · Esc solta o foco

Por que funciona. (a0+S1)(b0+S2)=a0b0+(a0S2+S1(b0+S2))(a_0 + S_1)(b_0 + S_2) = a_0 b_0 + \big(a_0 S_2 + S_1 \cdot (b_0 + S_2)\big): a cabeça é o produto das cabeças; a cauda soma "a0a_0 vezes o resto de s2s_2" com "o resto de s1s_1 vezes s2s_2 inteiro". A verificação imprime 1,0,0,0,0,01, 0, 0, 0, 0, 0 — a identidade pitagórica, coeficiente a coeficiente.

Exercício 3.61

Seja SS uma série de potências (exercício 3.59) cujo termo constante é 1. Suponha que queremos encontrar a série de potências 1/S1/S, isto é, a série XX tal que SX=1SX = 1. Escreva S=1+SRS = 1 + S_R onde SRS_R é a parte de SS após o termo constante. Então podemos resolver para XX da seguinte forma:

SX=1(1+SR)X=1X+SRX=1X=1SRX\begin{align*} S \cdot X &= 1 \\ (1 + S_R) \cdot X &= 1 \\ X + S_R \cdot X &= 1 \\ X &= 1 - S_R \cdot X \end{align*}

Em outras palavras, XX é a série de potências cujo termo constante é 1 e cujos termos de ordem superior são o negativo de SRS_R vezes XX. Use esta ideia para escrever uma função invert_unit_series que computa 1/S1/S para uma série de potências SS com termo constante 1. Você precisará usar mul_series do exercício 3.60.

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

A dedução do enunciado vira código diretamente — X=1SRXX = 1 - S_R \cdot X:

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

A definição parece circular — invert_unit_series(s) aparece no próprio corpo — mas a cabeça (1) está pronta antes de qualquer cauda ser pedida, e cada coeficiente novo só depende dos anteriores. O produto verifica: 1,0,0,0,1, 0, 0, 0, \ldots

Exercício 3.62

Use os resultados dos exercícios 3.60 e 3.61 para definir uma função div_series que divide duas séries de potências. A função div_series deve funcionar para qualquer duas séries, desde que o denominador comece com um termo constante não zero. (Se o denominador tiver um termo constante zero, então div_series deve sinalizar um erro.) Mostre como usar div_series juntamente com os resultados do exercício 3.59 para gerar a série de potências para a tangente.

💡 Mostrar solução — tente primeiro!💡 Esconder solução
Exemplo de Código
Código editável · Ctrl+Enter executa · Esc solta o foco

Por que funciona. Normalizamos o denominador para termo constante 1 (dividindo por cc), invertemos com o exercício 3.61, multiplicamos pelo numerador e desfazemos a normalização. A tangente sai 0,1,0,13,0,215,0,173150, 1, 0, \tfrac13, 0, \tfrac2{15}, 0, \tfrac{17}{315} — os coeficientes clássicos.

📝 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. Eratóstenes, um filósofo grego alexandrino do século III aC, é famoso por dar a primeira estimativa precisa da circunferência da Terra, que ele computou observando sombras lançadas ao meio-dia no dia do solstício de verão. O método de crivo de Eratóstenes, embora antigo, formou a base para "crivos" de hardware de propósito especial que, até os anos 1970, eram as ferramentas mais poderosas existentes para localizar grandes primos. Desde então, no entanto, esses métodos foram suplantados por derivações das técnicas probabilísticas discutidas na seção 1.2.6.

  2. Isso usa a função stream_map_2 do exercício 3.50.