3.5.4 Streams e Avaliação Atrasada
A função integral no final da seção anterior mostra como podemos usar streams para modelar sistemas de processamento de sinais que contêm loops de realimentação. O loop de realimentação para o somador mostrado na figura é modelado pelo fato de que o stream interno integ é definido em termos de si mesmo:
const integ = pair(initial_value,
() => add_streams(scale_stream(integrand, dt),
integ));
A capacidade do interpretador de lidar com tal definição implícita depende do atraso resultante de embrulhar a chamada para add_streams em uma expressão lambda. Sem esse atraso, o interpretador não poderia construir integ antes de avaliar a chamada para add_streams, o que exigiria que integ já estivesse definido. Em geral, tal atraso é crucial para usar streams para modelar sistemas de processamento de sinais que contêm loops. Sem um atraso, nossos modelos teriam que ser formulados de modo que as entradas para qualquer componente de processamento de sinal fossem totalmente avaliadas antes que a saída pudesse ser produzida. Isso proibiria loops.
Infelizmente, modelos de stream de sistemas com loops podem exigir usos de atraso além do padrão de programação de stream visto até agora. Por exemplo, a figura mostra um sistema de processamento de sinais para resolver a equação diferencial onde é uma função dada. A figura mostra um componente de mapeamento, que aplica ao seu sinal de entrada, ligado em um loop de realimentação a um integrador de maneira muito similar à dos circuitos de computador analógico que são realmente usados para resolver tais equações.
Figura 3.54: Sistema de processamento de sinais com loop de realimentação para resolver . O sinal de entrada dy/dt é integrado para produzir y, que passa por uma função de ganho f (representando a função diferencial), e retorna ao somador através de um loop de realimentação tracejado.
Assumindo que nos é dado um valor inicial para , poderíamos tentar modelar este sistema usando a função
function solve(f, y0, dt) {
const y = integral(dy, y0, dt);
const dy = stream_map(f, y);
return y;
}
Esta função não funciona, porque na primeira linha de solve a chamada para integral requer que o argumento de entrada dy seja definido, o que não acontece até a segunda linha de solve.
Por outro lado, a intenção de nossa definição faz sentido, porque podemos, em princípio, começar a gerar o stream y sem conhecer dy. De fato, integral e muitas outras operações de stream podem gerar parte da resposta dada apenas informação parcial sobre os argumentos. Para integral, o primeiro elemento do stream de saída é o initial_value especificado. Assim, podemos gerar o primeiro elemento do stream de saída sem avaliar o integrando dy. Uma vez que conhecemos o primeiro elemento de y, o stream_map na segunda linha de solve pode começar a trabalhar para gerar o primeiro elemento de dy, que produzirá o próximo elemento de y, e assim por diante.
Para tirar vantagem dessa ideia, redefiniremos integral para esperar que o stream do integrando seja um argumento atrasado. A função integral forçará o integrando a ser avaliado apenas quando for necessário para gerar mais do que o primeiro elemento do stream de saída:
function integral(delayed_integrand, initial_value, dt) {
const integ =
pair(initial_value,
() => {
const integrand = delayed_integrand();
return add_streams(scale_stream(integrand, dt),
integ);
});
return integ;
}
Agora podemos implementar nossa função solve atrasando a avaliação de dy na declaração de y:
function solve(f, y0, dt) {
const y = integral(() => dy, y0, dt);
const dy = stream_map(f, y);
return y;
}
Em geral, cada chamador de integral deve agora atrasar o argumento do integrando. Podemos demonstrar que a função solve funciona aproximando computando o valor em da solução para a equação diferencial com condição inicial :1
stream_ref(solve(y => y, 1, 0.001), 1000);
2.716923932235896
Exercício 3.77
A função integral usada acima foi análoga à definição "implícita" do stream infinito de inteiros na seção 3.5.2. Alternativamente, podemos dar uma definição de integral que é mais como integers_starting_from (também na seção 3.5.2):
function integral(integrand, initial_value, dt) {
return pair(initial_value,
is_null(integrand)
? null
: integral(stream_tail(integrand),
dt * head(integrand) + initial_value,
dt));
}
Quando usada em sistemas com loops, esta função tem o mesmo problema que nossa versão original de integral. Modifique a função para que ela espere o integrand como um argumento atrasado e, portanto, possa ser usada na função solve mostrada acima.
💡 Mostrar solução — tente primeiro!💡 Esconder solução
Basta adiar também o integrand nesta formulação, chamando-o quando a cauda for construída:
Por que funciona. Cada passo da recursão recebe o integrando ainda embrulhado (delayed_integrand) e só o abre dentro da cauda adiada — assim solve pode definir y e dy mutuamente sem que nenhum dos dois precise existir primeiro. O resultado () aproxima .
Exercício 3.78
Considere o problema de projetar um sistema de processamento de sinais para estudar a equação diferencial linear homogênea de segunda ordem
O stream de saída, modelando , é gerado por uma rede que contém um loop. Isso ocorre porque o valor de depende dos valores de e e ambos são determinados integrando . O diagrama que gostaríamos de codificar é mostrado na figura.
Figura 3.78: Sistema de processamento de sinais para resolver sistemas de equações diferenciais acopladas. Dois integradores processam dy₁ e dy₂ para produzir y₁ e y₂, com multiplicadores de ganho a e b. Os loops de realimentação cruzados (tracejados) mostram como cada equação depende da solução da outra.
Escreva uma função solve_2nd que recebe como argumentos as constantes , e e os valores iniciais e para e e gera o stream de valores sucessivos de .
💡 Mostrar solução — tente primeiro!💡 Esconder solução
Seguindo o diagrama: ddy é a combinação a·dy + b·y, e dy e y são integrais encadeadas de ddy:
As três funções são mutuamente recursivas e nenhuma executa nada até o stream ser puxado — a rede com loop do diagrama vira, literalmente, três declarações que se referenciam. Com a solução é , e o valor em sai .
Exercício 3.79
Generalize a função solve_2nd do exercício 3.78 para que ela possa ser usada para resolver equações diferenciais de segunda ordem gerais .
💡 Mostrar solução — tente primeiro!💡 Esconder solução
A generalização troca a combinação linear fixa por uma função arbitrária de dy e y:
stream_map_2(f, dy(), y()) alimenta com os valores correntes de e — qualquer equação , linear ou não, entra na mesma rede.
Exercício 3.80
Um circuito RLC em série consiste de um resistor, um capacitor e um indutor conectados em série, como mostrado na figura. Se , e são a resistência, indutância e capacitância, então as relações entre voltagem () e corrente () para os três componentes são descritas pelas equações
e as conexões do circuito ditam as relações
Combinando essas equações mostra que o estado do circuito (resumido por , a voltagem através do capacitor, e , a corrente no indutor) é descrito pelo par de equações diferenciais
O diagrama de fluxo de sinal representando este sistema de equações diferenciais é mostrado na figura.
Figura 3.80: Circuito RLC em série mostrando resistor (R), indutor (L) e capacitor (C) conectados em série com uma fonte de tensão V(t). O circuito demonstra a relação entre voltagem e corrente nos três componentes passivos fundamentais da eletrônica.
Escreva uma função RLC que recebe como argumentos os parâmetros , e do circuito e o incremento de tempo . De maneira similar à função RC do exercício 3.73, RLC deve produzir uma função que recebe os valores iniciais das variáveis de estado, e , e produz um par (usando pair) dos streams de estados e . Usando RLC, gere o par de streams que modela o comportamento de um circuito RLC em série com ohm, farad, henry, segundo, e valores iniciais amperes e volts.
Avaliação em ordem normal
Os exemplos nesta seção ilustram como a avaliação atrasada fornece grande flexibilidade de programação, mas os mesmos exemplos também mostram como isso pode tornar nossos programas mais complexos. Nossa nova função integral, por exemplo, nos dá o poder de modelar sistemas com loops, mas devemos agora lembrar que integral deve ser chamada com um integrando atrasado, e cada função que usa integral deve estar ciente disso. Em efeito, criamos duas classes de funções: funções ordinárias e funções que recebem argumentos atrasados. Em geral, criar classes separadas de funções nos força a criar classes separadas de funções de ordem superior também.2
Uma maneira de evitar a necessidade de duas classes diferentes de funções é fazer todas as funções receberem argumentos atrasados. Poderíamos adotar um modelo de avaliação no qual todos os argumentos para funções são automaticamente atrasados e os argumentos são forçados apenas quando são realmente necessários (por exemplo, quando são exigidos por uma operação primitiva). Isso transformaria nossa linguagem para usar avaliação em ordem normal, que descrevemos pela primeira vez quando introduzimos o modelo de substituição para avaliação na seção 1.1.5. Converter para avaliação em ordem normal fornece uma maneira uniforme e elegante de simplificar o uso de avaliação atrasada, e esta seria uma estratégia natural a adotar se estivéssemos preocupados apenas com o processamento de streams. Na seção 4.2, após estudarmos o avaliador, veremos como transformar nossa linguagem exatamente dessa maneira. Infelizmente, incluir atrasos em chamadas de função causa estragos com nossa capacidade de projetar programas que dependem da ordem de eventos, como programas que usam atribuição, mutam dados ou executam entrada ou saída. Mesmo um único atraso no tail de um par pode causar grande confusão, como ilustrado pelos exercícios 3.51 e 3.52. Até onde se sabe, mutabilidade e avaliação atrasada não se misturam bem em linguagens de programação.
💡 Mostrar solução — tente primeiro!💡 Esconder solução
Das leis do circuito: e — duas integrais acopladas:
RLC(1, 1, 0.2, 0.1) devolve o gerador de circuitos pedido; aplicado às condições iniciais , , produz o par de streams: começa em 10 e decai; começa em 0, sobe para 1 no passo seguinte ( com ) e passa a oscilar amortecido.