Pular para o conteúdo

51 · Programação reativa

15 exercícios: .

bash
python3 exercicios/run_all.py 51

Os exercícios#

#TítuloEnunciado
283um valor que outros valores acompanhama diferenca entre "me avise quando algo acontecer" e
284a dependencia e DESCOBERTA, nao declaradanao ha lista para escrever. O derivado roda, e todo sinal
285o efeito roda ao nascer, e o lote agrupaum efeito que nao rodasse na criacao deixaria a tela sem
286o valor que nunca existiunum losango — 'c' le 'a' e 'b', e 'b' le 'a' — marcar e
287sinal e valor; observavel e fluxoum clique e fluxo — perguntar "qual o valor do clique
288a fonte fria liga preguicoso'de_cluster' so comeca quando alguem escuta. Se o
289o tempo dentro do fluxo'esperar' e o debounce — so emite quando o fluxo fica
290dois fluxos num so, de duas formas'juntar' intercala — o que vier, de qualquer um.
291a falha ENCERRA o fluxonum fluxo, um erro nao pode derrubar o programa — quem
292o que o grafo reativo recusatres recusas, e cada uma evita um defeito que nao
293um carrinho que se recalcula sozinhojuntar sinal, derivado, efeito e lote num caso de uso —
294quando "mudou" nao e obvioescrever o mesmo valor nao notifica. Mas o que e "o
295o reativo em cima de uma tabelaum painel que filtra e resume. O filtro e um sinal, a
296o fluxo que bate sozinho'intervalo' e uma fonte fria com relogio. Ela e a forma
297quatro formas de lidar com mudancaa linguagem tem 'Arcane.Eventos' (um emissor),

283 · um valor que outros valores acompanham#

Enunciado. a diferenca entre "me avise quando algo acontecer" e

exercicios/51-reativo/283_sinal_e_derivado.df
// "este total e sempre a soma daqueles tres". O derivado e preguicoso
// e memorizado — e contar as vezes que a formula roda e o unico jeito
// de PROVAR as duas coisas.

adopt Arcane.Reativo as R

steady preco := R.sinal(10.0)
steady quantidade := R.sinal(3)

contas := {"n": 0}

action calcular():
    contas["n"] := contas["n"] + 1
    yield preco.ler() * quantidade.ler()

steady total := R.derivado(calcular)

out "== 1. ele so calcula quando alguem LE =="

assert contas["n"] is 0
assert total.ler() is 30.0
assert contas["n"] is 1

out ""
out "== 2. e o valor fica memorizado =="

total.ler()
total.ler()
assert contas["n"] is 1
out "   tres leituras, uma conta so"

out ""
out "== 3. mudar uma fonte invalida =="

quantidade.escrever(5)
assert total.ler() is 50.0
assert contas["n"] is 2

out ""
out "== 4. escrever o MESMO valor nao invalida =="

// Senao a cadeia recalcularia por nada, e um efeito de rede dispararia
// duas vezes pelo mesmo estado.
quantidade.escrever(5)
assert total.ler() is 50.0
assert contas["n"] is 2

out ""
out "== 5. atualizar le e escreve numa chamada =="

quantidade.atualizar(lambda q => q + 1)
assert quantidade.ler() is 6
assert total.ler() is 60.0

out ""
out "== 6. ler SEM depender =="

// '.valor()' nao registra a dependencia: e a saida para um contador de
// depuracao que nao deve fazer o derivado recalcular.
assert preco.valor() is 10.0
assert quantidade.valor() is 6

out "exercicio 283 ok"

A diferença entre "me avise quando algo acontecer" e "este total é sempre a soma daqueles três". Contar as vezes que a fórmula roda é o único jeito de provar a preguiça e a memória.

Ele só calcula quando alguém lê#

Recalcular na escrita faria uma cadeia de dez derivados rodar dez vezes por mudança, e a maioria deles nunca é lida.

E o valor fica memorizado#

Três leituras, uma conta. O contador é a prova; sem ele isso seria uma afirmação.

Escrever o mesmo valor não invalida#

Senão a cadeia recalcularia por nada, e um efeito de rede dispararia duas vezes pelo mesmo estado.

E `.valor()` lê sem depender#

É o untracked dos outros frameworks: sem ele, um derivado que lê um contador de depuração passaria a recalcular a cada incremento dele.

284 · a dependencia e DESCOBERTA, nao declarada#

Enunciado. nao ha lista para escrever. O derivado roda, e todo sinal

exercicios/51-reativo/284_dependencia_descoberta.df
// lido durante a execucao entra. Uma lista a mao envelhece na primeira
// condicao nova dentro da formula — e o sintoma e um valor que para de
// atualizar.

adopt Arcane.Reativo as R

steady subtotal := R.sinal(50.0)
steady cupom := R.sinal(0.0)

contas := {"n": 0}

action calcular_frete():
    contas["n"] := contas["n"] + 1
    given subtotal.ler() bigger 100.0:
        yield 0.0
    yield 20.0 - cupom.ler()

steady frete := R.derivado(calcular_frete)

out "== 1. abaixo de 100, o frete LE o cupom =="

assert frete.ler() is 20.0
antes := contas["n"]

cupom.escrever(5.0)
assert frete.ler() is 15.0
assert contas["n"] is antes + 1
out "   mexer no cupom recalculou o frete"

out ""
out "== 2. acima de 100 a formula toma o outro ramo =="

subtotal.escrever(300.0)
assert frete.ler() is 0.0

out ""
out "== 3. e a partir dai o cupom nao mexe em nada =="

// As fontes que sumiram param de notificar. Sem isso, a formula
// acumularia dependencias dos DOIS ramos e recalcularia por mudancas
// que ela nem le mais.
antes := contas["n"]
cupom.escrever(7.0)
frete.ler()
assert contas["n"] is antes
out "   o cupom saiu das dependencias"

out ""
out "== 4. voltando abaixo de 100, ele volta =="

subtotal.escrever(50.0)
assert frete.ler() is 13.0
antes := contas["n"]
cupom.escrever(1.0)
assert frete.ler() is 19.0
assert contas["n"] is antes + 1

out ""
out "== 5. um derivado sobre derivados =="

steady total := R.derivado(lambda => subtotal.ler() + frete.ler())
assert total.ler() is 69.0
subtotal.escrever(60.0)
assert total.ler() is 79.0

out "exercicio 284 ok"

Não há lista para escrever: o derivado roda, e todo sinal lido durante a execução entra. Uma lista à mão envelhece na primeira condição nova dentro da fórmula — e o sintoma é um valor que para de atualizar.

O ramo tomado decide a dependência#

Abaixo de 100 o frete lê o cupom, e mexer no cupom o invalida. Acima de 100 a fórmula toma o outro ramo e não lê mais.

As fontes que sumiram param de notificar#

Sem isso, a fórmula acumularia as dependências dos dois ramos e recalcularia por mudanças que ela nem lê mais.

E voltando, a dependência volta#

Ela é refeita a cada execução: não há estado antigo a limpar.

285 · o efeito roda ao nascer, e o lote agrupa#

Enunciado. um efeito que nao rodasse na criacao deixaria a tela sem

exercicios/51-reativo/285_efeito_e_lote.df
// o estado inicial — e quem escreve teria de chama-lo a mao, o que se
// esquece exatamente uma vez. E tres escritas em sequencia mostram dois
// estados intermediarios que nunca deveriam aparecer.

adopt Arcane.Reativo as R

steady a := R.sinal(1)
steady b := R.sinal(10)
steady soma := R.derivado(lambda => a.ler() + b.ler())

vistos := []

action anotar():
    vistos.append(soma.ler())

steady vigia := R.efeito(anotar)

out "== 1. ele ja rodou =="

assert vistos is [11]

out ""
out "== 2. e roda de novo a cada mudanca =="

a.escrever(2)
assert vistos is [11, 12]
b.escrever(20)
assert vistos is [11, 12, 22]

out ""
out "== 3. o lote agrupa varias escritas numa notificacao =="

action tres_escritas():
    a.escrever(3)
    b.escrever(30)
    a.escrever(4)

R.lote(tres_escritas)
assert vistos is [11, 12, 22, 34]
out "   tres escritas, uma notificacao"

out ""
out "== 4. o lote devolve o que a acao devolveu =="

assert R.lote(lambda => 42) is 42

out ""
out "== 5. parar e definitivo =="

vigia.parar()
a.escrever(100)
assert vistos is [11, 12, 22, 34]
assert soma.ler() is 130
out "   o derivado continua certo; o efeito e que parou"

out ""
out "== 6. um efeito que nao le sinal nenhum roda uma vez so =="

// Sem dependencia descoberta, nada tem como acorda-lo de novo.
quantas := {"n": 0}

action so_conta():
    quantas["n"] := quantas["n"] + 1

steady solto := R.efeito(so_conta)
assert quantas["n"] is 1

a.escrever(200)
b.escrever(300)
assert quantas["n"] is 1
out "   sem dependencia, nada o acorda"

out "exercicio 285 ok"

Um efeito que não rodasse na criação deixaria a tela sem o estado inicial — e quem escreve teria de chamá-lo à mão, o que se esquece exatamente uma vez.

Ele já rodou#

E roda de novo a cada mudança de qualquer dependência.

O lote agrupa várias escritas numa notificação#

Sem ele, três escritas mostram dois estados intermediários que nunca deveriam aparecer na tela. E ele já existiu sem agrupar nada: montava uma lista de adiados que ninguém lia.

Parar é definitivo#

O derivado continua certo; o que parou foi o efeito. São coisas diferentes, e confundi-las faz alguém "desligar" o cálculo achando que desligou o desenho.

E um efeito sem dependência roda uma vez só#

Sem sinal lido, nada tem como acordá-lo.

286 · o valor que nunca existiu#

Enunciado. num losango — 'c' le 'a' e 'b', e 'b' le 'a' — marcar e

exercicios/51-reativo/286_losango.df
// avisar numa fase so entrega um numero errado na tela, que aparece e
// some sozinho. A propagacao tem DUAS fases: marcar o grafo inteiro e,
// so entao, avisar.

adopt Arcane.Reativo as R

steady a := R.sinal(1)
steady b := R.derivado(lambda => a.ler() * 2)
steady c := R.derivado(lambda => a.ler() + b.ler())

vistos := []
steady vigia := R.efeito(lambda => vistos.append(c.ler()))

out "== 1. o estado inicial =="

assert vistos is [3]  // ou seja, um mais dois

out ""
out "== 2. uma escrita, UM aviso, e ele consistente =="

a.escrever(5)
// O certo e 15 (5 + 10). Antes da correcao o efeito via 7 primeiro —
// o 'a' novo somado ao 'b' velho —, e esse 7 nunca foi verdade.
assert vistos is [3, 15]
out $"   {vistos}"

out ""
out "== 3. o efeito alcancado por dois caminhos roda UMA vez =="

a.escrever(10)
assert len(vistos) is 3
assert vistos[-1] is 30

out ""
out "== 4. uma cadeia longa tambem avisa uma vez =="

steady d1 := R.derivado(lambda => a.ler() + 1)
steady d2 := R.derivado(lambda => d1.ler() + 1)
steady d3 := R.derivado(lambda => d2.ler() + 1)
steady d4 := R.derivado(lambda => d3.ler() + 1)

quantas := []
steady fim := R.efeito(lambda => quantas.append(d4.ler()))
assert quantas is [14]

a.escrever(100)
assert quantas is [14, 104]

out ""
out "== 5. o ouvinte de um derivado espera a marcacao =="

lidos := []
c.observar(lambda v => lidos.append(v))
a.escrever(7)
assert lidos is [21]

out "exercicio 286 ok"

Num losango — ca e b, e ba — marcar e avisar numa fase só entrega um número errado, que aparece e some sozinho.

O 7 nunca foi verdade#

Com a = 5, o certo é 15. O 7 era 5 + o b antigo: b ainda estava limpo quando c recalculou. E a lista de dependentes é um conjunto, então qual caminho vem primeiro não é escolhido por ninguém — o defeito ia e vinha conforme a ordem de hash.

A marcação percorre o grafo inteiro primeiro#

Só depois os efeitos e ouvintes rodam. Quando eles rodam, todo derivado alcançado já sabe que está sujo.

E o efeito alcançado por dois caminhos roda uma vez#

Deduplicado pelo próprio objeto — e não por id(), que só é único entre objetos vivos.

---

Não é uma notificação a mais: é um valor errado na tela. Um defeito que produz o número certo depois é mais difícil de achar que um que estoura.

287 · sinal e valor; observavel e fluxo#

Enunciado. um clique e fluxo — perguntar "qual o valor do clique

exercicios/51-reativo/287_observavel.df
// agora?" nao tem resposta. Um saldo e valor. Frameworks que chamam os
// dois de "stream" fazem a pergunta "qual e o valor atual?" deixar de
// ter resposta.

adopt Arcane.Reativo as R

steady cliques := R.observavel("cliques")
recebidos := []

out "== 1. quem se inscreve recebe o que vier DAQUI PARA A FRENTE =="

cliques.emitir({"botao": "ver", "produto": "cafe"})  // ninguem ouvia

inscricao := cliques.inscrever(lambda c => recebidos.append(c["produto"]))
cliques.emitir({"botao": "comprar", "produto": "moedor"})
assert recebidos is ["moedor"]

out ""
out "== 2. os operadores sao os mesmos nomes do pipeline =="

compras := []
so_compras := cliques.sift(lambda c => c["botao"] is "comprar")
nomes := so_compras.morph(lambda c => c["produto"])
outra := nomes.distintos().inscrever(lambda p => compras.append(p))

cliques.emitir({"botao": "ver", "produto": "prensa"})
cliques.emitir({"botao": "comprar", "produto": "cafe"})
cliques.emitir({"botao": "comprar", "produto": "cafe"})
cliques.emitir({"botao": "comprar", "produto": "filtro"})

assert compras is ["cafe", "filtro"]
out $"   {compras}"

out ""
out "== 3. cancelar para de chegar =="

outra.cancelar()
cliques.emitir({"botao": "comprar", "produto": "coador"})
assert compras is ["cafe", "filtro"]
assert len(recebidos) bigger 1

out ""
out "== 4. o fluxo vira valor quando a pergunta muda =="

ultimo := cliques.para_sinal({"produto": "nenhum"})
assert ultimo.ler()["produto"] is "nenhum"
cliques.emitir({"botao": "comprar", "produto": "bule"})
assert ultimo.ler()["produto"] is "bule"

out ""
out "== 5. encerrar e definitivo =="

inscricao.cancelar()
cliques.encerrar()

fechado := no
monitor:
    cliques.emitir({"botao": "comprar", "produto": "tarde"})
handle StreamClosedError as e:
    fechado := yes
    assert "encerrado" in e.message
assert fechado

out "exercicio 287 ok"

Um clique é fluxo — perguntar "qual o valor do clique agora?" não tem resposta. Um saldo é valor. Frameworks que chamam os dois de "stream" fazem a pergunta "qual é o valor atual?" deixar de ter resposta.

Quem se inscreve recebe o que vier daqui para a frente#

O que foi emitido antes não volta. É a definição de fluxo.

Os operadores têm os nomes do pipeline#

morph, sift, distill — os mesmos do >> da linguagem, e não um segundo vocabulário.

O fluxo vira valor quando a pergunta muda#

para_sinal é a ponte: a partir dali, "qual é o último?" tem resposta.

E encerrar é definitivo#

Emitir depois disso levanta. Devolver no calado fazia o valor sumir sem nada no log.

288 · a fonte fria liga preguicoso#

Enunciado. 'de_cluster' so comeca quando alguem escuta. Se o

exercicios/51-reativo/288_fonte_fria.df
// operador se ligasse a fonte na CONSTRUCAO, os valores sairiam antes
// de o assinante final existir — e o resultado seria uma lista vazia,
// sem erro nenhum.

adopt Arcane.Reativo as R

// Nomeado porque o lint tem razao: um 4 solto em cinco lugares e
// cinco decisoes que se perdem de vista.
steady ATE_SEIS := [1, 2, 3, 4, 5, 6]
steady ATE_CINCO := [1, 2, 3, 4, 5]
steady ATE_QUATRO := [1, 2, 3, 4]

out "== 1. a cadeia inteira chega =="

dobrados := []
pares := R.de_cluster(ATE_SEIS).sift(lambda n => n % 2 is 0)
pares.morph(lambda n => n * 10).inscrever(lambda n => dobrados.append(n))
assert dobrados is [20, 40, 60]

out ""
out "== 2. sem ninguem escutando, nada corre =="

correu := []
frio := R.de_cluster([1, 2, 3]).morph(lambda n => rastrear(n, correu))

action rastrear(n, caixa):
    caixa.append(n)
    yield n

assert correu is []
out "   a fonte fria esperou"

frio.inscrever(lambda _n => void)
assert correu is [1, 2, 3]

out ""
out "== 3. primeiros e pular recortam =="

tres := []
R.de_cluster(ATE_CINCO).primeiros(3).inscrever(lambda n => tres.append(n))
assert tres is [1, 2, 3]

resto := []
R.de_cluster(ATE_CINCO).pular(3).inscrever(lambda n => resto.append(n))
assert resto is [4, 5]

out ""
out "== 4. blocos junta em grupos =="

grupos := []
R.de_cluster(ATE_CINCO).blocos(2).inscrever(lambda g => grupos.append(g))
assert grupos is [[1, 2], [3, 4]]
out "   o resto incompleto nao e emitido"

out ""
out "== 5. distill acumula ao longo do fluxo =="

somas := []
R.de_cluster(ATE_QUATRO).distill(lambda a, v => a + v, 0)
    .inscrever(lambda s => somas.append(s))
assert somas is [1, 3, 6, 10]

out "exercicio 288 ok"

de_cluster só começa quando alguém escuta. Se o operador se ligasse à fonte na construção, os valores sairiam antes de o assinante final existir — e o resultado seria uma lista vazia, sem erro nenhum.

A cadeia inteira chega#

Porque cada operador só se conecta à sua fonte ao receber o primeiro inscrito.

Sem ninguém escutando, nada corre#

A prova é um efeito colateral dentro do morph: ele não aconteceu.

E os recortes#

primeiros, pular, blocos e distill — o blocos não emite o resto incompleto, porque um bloco pela metade não é um bloco.

289 · o tempo dentro do fluxo#

Enunciado. 'esperar' e o debounce — so emite quando o fluxo fica

exercicios/51-reativo/289_esperar_e_limitar.df
// quieto; 'limitar' e o throttle — deixa passar no maximo um por
// intervalo. Os dois existem porque um campo de busca que consulta a
// cada tecla manda vinte consultas para escrever "cafeteira".

adopt Arcane.Reativo as R
adopt Arcane.Time as Tempo

steady QUIETO := 0.12
steady PRAZO := 5.0

action esperar_ate(condicao):
    """Espera a condicao, e nao o relogio: um 'sleep' fixo mede a
    maquina, e o mesmo exercicio reprova numa esteira carregada."""
    limite := Tempo.timestamp() + PRAZO
    persist Tempo.timestamp() smaller limite:
        given condicao():
            yield yes
        Tempo.sleep(0.01)
    yield no

out "== 1. esperar so emite depois do silencio =="

teclas := R.observavel("teclas")
buscas := []
teclas.esperar(QUIETO).inscrever(lambda t => buscas.append(t))

cycle letra in ["c", "ca", "caf", "cafe"]:
    teclas.emitir(letra)

// Nada saiu ainda: o fluxo nao ficou quieto.
assert buscas is []
assert esperar_ate(lambda => len(buscas) is 1)
assert buscas is ["cafe"]
out $"   quatro teclas, uma busca: {buscas}"

out ""
out "== 2. um segundo silencio emite de novo =="

teclas.emitir("cha")
assert esperar_ate(lambda => len(buscas) is 2)
assert buscas is ["cafe", "cha"]

out ""
out "== 3. limitar deixa passar o PRIMEIRO de cada intervalo =="

rolagem := R.observavel("rolagem")
marcos := []
rolagem.limitar(QUIETO).inscrever(lambda n => marcos.append(n))

cycle i from 1 to 5:
    rolagem.emitir(i)

assert marcos is [1]
out "   cinco eventos de rolagem, um marco"

Tempo.sleep(QUIETO * 2)
rolagem.emitir(99)
assert esperar_ate(lambda => len(marcos) is 2)
assert marcos is [1, 99]

out ""
out "== 4. a diferenca, em uma frase =="

// 'esperar' responde no FIM da rajada; 'limitar' responde no COMECO.
assert buscas[0] is "cafe"  // a ultima tecla
assert marcos[0] is 1  // o primeiro evento

out "exercicio 289 ok"

esperar é o debounce — só emite quando o fluxo fica quieto; limitar é o throttle — deixa passar no máximo um por intervalo.

A diferença, em uma frase#

esperar responde no fim da rajada; limitar responde no começo.

E os dois existem pelo mesmo motivo#

Um campo de busca que consulta a cada tecla manda vinte consultas para escrever "cafeteira".

O teste espera por CONDIÇÃO#

Um sleep fixo mede a máquina, e o mesmo exercício reprova numa esteira carregada sem que nada tenha mudado no código.

290 · dois fluxos num so, de duas formas#

Enunciado. 'juntar' intercala — o que vier, de qualquer um.

exercicios/51-reativo/290_juntar_e_combinar.df
// 'combinar' emite o ULTIMO de cada, e so depois que todos ja
// emitiram pelo menos uma vez: antes disso nao ha "ultimo" para um
// deles, e inventar um seria mentir sobre um valor que nunca existiu.

adopt Arcane.Reativo as R

out "== 1. juntar intercala =="

a := R.observavel("a")
b := R.observavel("b")
tudo := []
R.juntar(a, b).inscrever(lambda v => tudo.append(v))

a.emitir(1)
b.emitir("x")
a.emitir(2)
b.emitir("y")

assert tudo is [1, "x", 2, "y"]

out ""
out "== 2. combinar espera todos falarem uma vez =="

c := R.observavel("c")
d := R.observavel("d")
pares := []
R.combinar(c, d).inscrever(lambda p => pares.append(p))

c.emitir(1)
assert pares is []  // 'd' ainda nao falou
c.emitir(2)
assert pares is []

d.emitir("x")
assert pares is [[2, "x"]]
out $"   {pares}"

out ""
out "== 3. e dai em diante, qualquer um dispara =="

c.emitir(3)
assert pares is [[2, "x"], [3, "x"]]
d.emitir("y")
assert pares is [[2, "x"], [3, "x"], [3, "y"]]

out ""
out "== 4. um formulario com dois campos =="

nome := R.observavel("nome")
email := R.observavel("email")
validos := []

action conferir(par):
    ok := len(par[0]) bigger 0 and "@" in par[1]
    validos.append(ok)

R.combinar(nome, email).inscrever(conferir)

nome.emitir("Ana")
email.emitir("ana")
assert validos is [no]
email.emitir("ana@exemplo.com")
assert validos is [no, yes]

out "exercicio 290 ok"

juntar intercala — o que vier, de qualquer um. combinar emite o último de cada sempre que qualquer um emite.

Combinar espera todos falarem uma vez#

Antes disso não há "último" para um deles, e inventar um seria mentir sobre um valor que nunca existiu.

E daí em diante, qualquer um dispara#

O par carrega o valor novo de quem emitiu e o último de quem não emitiu.

Um formulário com dois campos#

É o caso canônico: a validação precisa dos dois, e nenhum dos dois sozinho responde.

291 · a falha ENCERRA o fluxo#

Enunciado. num fluxo, um erro nao pode derrubar o programa — quem

exercicios/51-reativo/291_erro_no_fluxo.df
// escuta esta num retorno de chamada, longe de qualquer 'monitor'.
// 'falhar' avisa quem se inscreveu E encerra: um fluxo que continuasse
// depois de falhar deixaria quem escuta sem saber se o proximo valor
// veio de uma fonte que ainda funciona.

adopt Arcane.Reativo as R

out "== 1. quem se inscreve pode receber o erro =="

leituras := R.observavel("leituras")
valores := []
falhas := []
fins := []

leituras.inscrever(lambda v => valores.append(v),
    lambda e => falhas.append(str(e)),
    lambda => fins.append(yes))

leituras.emitir(10)
leituras.falhar("o sensor nao respondeu")

assert valores is [10]
assert len(falhas) is 1
assert "sensor" in falhas[0]

out ""
out "== 2. e a falha ENCERRA o fluxo =="

// O aviso de fim sai junto: quem escuta sabe que nao vem mais nada.
assert fins is [yes]

acabou := no
monitor:
    leituras.emitir(20)
handle StreamClosedError:
    acabou := yes
assert acabou
out "   depois da falha, nao ha proximo valor"

out ""
out "== 3. ao_falhar transforma o erro em VALOR =="

sensor := R.observavel("sensor")
lidos := []
sensor.ao_falhar(lambda _e => -1).inscrever(lambda v => lidos.append(v))

sensor.emitir(21)
sensor.falhar("cabo solto")
assert lidos is [21, -1]
out $"   {lidos}"

// O fluxo continua encerrado: 'ao_falhar' traduz a falha, e nao a
// desfaz.
ainda := no
monitor:
    sensor.emitir(22)
handle StreamClosedError:
    ainda := yes
assert ainda

out ""
out "== 4. o fim sem erro tambem e avisado =="

fila := R.observavel("fila")
fim := []
fila.inscrever(lambda _v => void, void, lambda => fim.append(yes))

assert fim is []
assert fila.encerrar() is yes
assert fim is [yes]
assert fila.encerrar() is no  // encerrar de novo nao avisa de novo

out ""
out "== 5. um SINAL nao tem erro =="

// Ele tem um valor agora; "o saldo falhou" nao e um estado de saldo.
// Quem precisa disso guarda um Arcane.Resultado dentro do sinal.
saldo := R.sinal(100)
assert saldo.ler() is 100

out "exercicio 291 ok"

Num fluxo, um erro não pode derrubar o programa — quem escuta está num retorno de chamada, longe de qualquer monitor.

Falhar avisa E encerra#

Um fluxo que continuasse depois de falhar deixaria quem escuta sem saber se o próximo valor veio de uma fonte que ainda funciona. O aviso de fim sai junto.

`ao_falhar` transforma o erro em VALOR#

E não desfaz o encerramento: ele traduz a falha, e o fluxo continua fechado.

E um SINAL não tem erro#

Ele tem um valor agora; "o saldo falhou" não é um estado de saldo. Quem precisa disso guarda um Arcane.Resultado dentro do sinal.

292 · o que o grafo reativo recusa#

Enunciado. tres recusas, e cada uma evita um defeito que nao

exercicios/51-reativo/292_reativo_recusa.df
// levantaria erro nenhum — ele so entregaria o valor errado.

adopt Arcane.Reativo as R

out "== 1. um derivado que depende de si mesmo =="

caixa := {}
steady a := R.derivado(lambda => caixa["b"].ler() + 1)
steady b := R.derivado(lambda => a.ler() + 1)
caixa["b"] := b

ciclo := no
monitor:
    a.ler()
handle ReactiveCycleError as e:
    ciclo := yes
    // A cadeia INTEIRA: dizer so "ha um ciclo" manda procurar em toda
    // a formula.
    assert "→" in e.nota
    out $"   {e.nota}"
assert ciclo

out ""
out "== 2. um derivado nao pode ESCREVER =="

// A formula e lida para descobrir de que ela depende. Escrever de
// dentro dela faz a propagacao correr no meio da propria descoberta.
contador := R.sinal(0)
steady mau := R.derivado(lambda => contador.escrever(1))

escreveu := no
monitor:
    mau.ler()
handle ReactiveWriteError as e:
    escreveu := yes
    assert "efeito" in e.dica
assert escreveu
assert contador.ler() is 0

out ""
out "== 3. mas um EFEITO pode =="

// Ele nao tem valor a produzir, e a escrita dele abre a proxima onda.
origem := R.sinal(1)
eco := R.sinal(0)
steady espelho := R.efeito(lambda => eco.escrever(origem.ler() * 10))

assert eco.ler() is 10
origem.escrever(3)
assert eco.ler() is 30
espelho.parar()

out ""
out "== 4. emitir num fluxo encerrado =="

fluxo := R.observavel("f")
fluxo.encerrar()

tarde := no
monitor:
    fluxo.emitir(1)
handle StreamClosedError:
    tarde := yes
assert tarde

out ""
out "== 5. a base pega as tres =="

action pela_base(acao):
    monitor:
        acao()
    handle ReactiveError:
        yield yes
    yield no

assert pela_base(lambda => a.ler())
assert pela_base(lambda => mau.ler())
assert pela_base(lambda => fluxo.emitir(1))

out "exercicio 292 ok"

Três recusas, e cada uma evita um defeito que não levantaria erro nenhum — ele só entregaria o valor errado.

Um derivado que depende de si mesmo#

A mensagem traz a cadeia inteira: dizer só "há um ciclo" manda procurar em toda a fórmula, e o ciclo mais curto é o mais fácil de quebrar.

Um derivado não pode escrever#

A fórmula é lida para descobrir de que ela depende. Escrever de dentro dela faz a propagação correr no meio da própria descoberta: o grafo muda enquanto está sendo percorrido.

Mas um EFEITO pode#

Ele não tem valor a produzir, e a escrita dele abre a próxima onda — e não reentra na que está correndo.

293 · um carrinho que se recalcula sozinho#

Enunciado. juntar sinal, derivado, efeito e lote num caso de uso —

exercicios/51-reativo/293_reativo_carrinho.df
// e medir quantas vezes cada formula rodou, que e o unico jeito de
// provar que a preguica e real.

adopt Arcane.Reativo as R

steady itens := R.sinal([])
steady cupom := R.sinal(0)
steady FRETE_BASE := 2000
steady ISENCAO := 15000

contas := {"subtotal": 0, "frete": 0, "total": 0}

action calcular_subtotal():
    contas["subtotal"] := contas["subtotal"] + 1
    yield sum([i["centavos"] * i["qtd"] cycle i in itens.ler()])

steady subtotal := R.derivado(calcular_subtotal)

action calcular_frete():
    contas["frete"] := contas["frete"] + 1
    given subtotal.ler() bigger_eq ISENCAO:
        yield 0
    yield FRETE_BASE - cupom.ler()

steady frete := R.derivado(calcular_frete)

action calcular_total():
    contas["total"] := contas["total"] + 1
    yield subtotal.ler() + frete.ler()

steady total := R.derivado(calcular_total)

recibos := []
steady recibo := R.efeito(lambda => recibos.append(total.ler()))

out "== 1. o carrinho vazio =="

assert subtotal.ler() is 0
assert frete.ler() is FRETE_BASE
assert recibos is [2000]

out ""
out "== 2. dois produtos =="

itens.escrever([
        {"nome": "cafe", "centavos": 3250, "qtd": 2},
        {"nome": "filtro", "centavos": 900, "qtd": 1}
    ])
assert subtotal.ler() is 7400
assert total.ler() is 9400
assert recibos[-1] is 9400

out ""
out "== 3. o cupom so vale abaixo da isencao =="

cupom.escrever(500)
assert frete.ler() is 1500
assert total.ler() is 8900

out ""
out "== 4. passando da isencao, o frete zera e para de ler o cupom =="

itens.escrever([{"nome":"moedor", "centavos":24000, "qtd":1}])
assert frete.ler() is 0
assert total.ler() is 24000

antes := contas["frete"]
cupom.escrever(900)
frete.ler()
assert contas["frete"] is antes
out "   o cupom saiu das dependencias do frete"

out ""
out "== 5. o lote agrupa a montagem inteira =="

quantos_recibos := len(recibos)

action montar_de_novo():
    itens.escrever([{"nome":"prensa", "centavos":8000, "qtd":1}])
    cupom.escrever(0)
    itens.escrever([{"nome":"prensa", "centavos":8000, "qtd":2}])

R.lote(montar_de_novo)
assert len(recibos) is quantos_recibos + 1
assert total.ler() is 16000  // passou da isencao: frete zero
out $"   tres escritas, um recibo: {recibos[-1]}"

out ""
out "== 6. e as contas provam a preguica =="

// Cada formula rodou muito menos vezes do que houve escritas.
out $"   subtotal {contas['subtotal']}, frete {contas['frete']}, total {contas['total']}"
assert contas["subtotal"] smaller 10
assert contas["total"] smaller 12

out "exercicio 293 ok"

Sinal, derivado, efeito e lote num caso de uso — e as contagens que provam que a preguiça é real.

A isenção muda a dependência#

Acima do valor de isenção o frete deixa de ler o cupom, e mexer nele para de recalcular qualquer coisa.

O lote agrupa a montagem inteira#

Três escritas, um recibo.

E as contas fecham#

Cada fórmula rodou muito menos vezes do que houve escritas — e o número é medido, não afirmado.

294 · quando "mudou" nao e obvio#

Enunciado. escrever o mesmo valor nao notifica. Mas o que e "o

exercicios/51-reativo/294_sinal_igualdade.df
// mesmo" depende do valor: dois clusters com o mesmo conteudo sao
// iguais, e dois objetos sem igualdade declarada nao. O sinal aceita a
// comparacao de quem escreveu.

adopt Arcane.Reativo as R

out "== 1. o padrao compara por igualdade =="

n := R.sinal(10)
vezes := []
n.observar(lambda v => vezes.append(v))

n.escrever(10)
assert vezes is []
n.escrever(11)
assert vezes is [11]

out ""
out "== 2. dois clusters de mesmo conteudo sao o mesmo valor =="

xs := R.sinal([1, 2, 3])
mudou := []
xs.observar(lambda v => mudou.append(len(v)))

xs.escrever([1, 2, 3])
assert mudou is []
xs.escrever([1, 2, 3, 4])
assert mudou is [4]

out ""
out "== 3. mudar a lista NO LUGAR nao avisa ninguem =="

// O sinal guarda uma referencia, e a referencia nao mudou. E a mesma
// razao por que a vigia do depurador compara uma FOTO estrutural.
atual := xs.ler()
atual.append(5)
assert len(xs.ler()) is 5
assert mudou is [4]
out "   append nao e escrita: o sinal nao soube"

// A forma que avisa e escrever uma lista nova.
xs.escrever([...atual, 6])
assert mudou is [4, 6]

out ""
out "== 4. uma comparacao propria =="

// Duas leituras de sensor que diferem por menos de 0,5 grau sao "a
// mesma temperatura" — e sem isso a tela pisca a cada ruido.
action quase_igual(a, b):
    yield abs(a - b) smaller 0.5

temperatura := R.sinal(20.0, iguais := quase_igual)
avisos := []
temperatura.observar(lambda t => avisos.append(t))

temperatura.escrever(20.2)
temperatura.escrever(20.4)
assert avisos is []
temperatura.escrever(21.0)
assert avisos is [21.0]
out $"   tres leituras, um aviso: {avisos}"

out ""
out "== 5. cancelar o observador =="

parar := temperatura.observar(lambda t => avisos.append(t))
temperatura.escrever(30.0)
assert len(avisos) is 3
parar()
temperatura.escrever(40.0)
assert len(avisos) is 4

out "exercicio 294 ok"

Escrever o mesmo valor não notifica. Mas o que é "o mesmo" depende do valor.

Dois clusters de mesmo conteúdo são o mesmo valor#

A comparação padrão é a igualdade da linguagem, e ela é estrutural.

Mudar a lista NO LUGAR não avisa ninguém#

O sinal guarda uma referência, e a referência não mudou. É a mesma razão por que a vigia do depurador compara uma foto estrutural — e a forma que avisa é escrever uma lista nova.

E uma comparação própria#

Duas leituras de sensor que diferem por menos de meio grau são "a mesma temperatura" — e sem isso a tela pisca a cada ruído.

295 · o reativo em cima de uma tabela#

Enunciado. um painel que filtra e resume. O filtro e um sinal, a

exercicios/51-reativo/295_reativo_e_quadro.df
// tabela filtrada e um derivado, e o resumo e outro — mexer no filtro
// recalcula os dois, e so eles.

adopt Arcane.Reativo as R
adopt Arcane.Quadro as Q

steady VENDAS := [
    {"vendedor": "Ana", "regiao": "sul", "valor": 1200.0},
    {"vendedor": "Bia", "regiao": "sul", "valor": 800.0},
    {"vendedor": "Caio", "regiao": "norte", "valor": 2400.0},
    {"vendedor": "Dora", "regiao": "norte", "valor": 600.0},
    {"vendedor": "Eva", "regiao": "sul", "valor": 3000.0}
]

steady regiao := R.sinal("sul")
steady minimo := R.sinal(0.0)

contas := {"filtrar": 0, "resumir": 0}

action filtrar():
    contas["filtrar"] := contas["filtrar"] + 1
    r := regiao.ler()
    m := minimo.ler()
    yield [v cycle v in VENDAS given v["regiao"] is r and v["valor"] bigger_eq m]

steady filtradas := R.derivado(filtrar)

action resumir():
    contas["resumir"] := contas["resumir"] + 1
    linhas := filtradas.ler()
    given len(linhas) is 0:
        yield {"quantas": 0, "total": 0.0, "media": 0.0}
    total := sum([l["valor"] cycle l in linhas])
    yield {"quantas": len(linhas), "total": total,
        "media": round(total / len(linhas), 2)}

steady resumo := R.derivado(resumir)

out "== 1. o sul =="

assert resumo.ler()["quantas"] is 3
assert resumo.ler()["total"] is 5000.0
out $"   {resumo.ler()}"

out ""
out "== 2. trocar a regiao recalcula os dois =="

regiao.escrever("norte")
assert resumo.ler()["quantas"] is 2
assert resumo.ler()["total"] is 3000.0

out ""
out "== 3. o minimo tambem e uma dependencia =="

minimo.escrever(1000.0)
assert resumo.ler()["quantas"] is 1
assert filtradas.ler()[0]["vendedor"] is "Caio"

out ""
out "== 4. e o quadro le a mesma lista =="

quadro := Q.de_vaults(filtradas.ler())
assert len(quadro) is 1
assert quadro.colunas() is ["vendedor", "regiao", "valor"]

out ""
out "== 5. nada recalcula sem leitura =="

antes := contas["filtrar"]
regiao.escrever("sul")
minimo.escrever(0.0)
assert contas["filtrar"] is antes
assert resumo.ler()["quantas"] is 3
assert contas["filtrar"] is antes + 1
out "   duas escritas, uma conta"

out "exercicio 295 ok"

Um painel que filtra e resume: o filtro é um sinal, a tabela filtrada é um derivado, e o resumo é outro.

Mexer no filtro recalcula os dois#

E só eles: a fonte não é tocada.

Nada recalcula sem leitura#

Duas escritas seguidas, uma conta. É a preguiça valendo numa cadeia de dois níveis.

E o quadro lê a mesma lista#

Q.de_vaults recebe o que o derivado devolveu — sem conversão, sem cópia.

296 · o fluxo que bate sozinho#

Enunciado. 'intervalo' e uma fonte fria com relogio. Ela e a forma

exercicios/51-reativo/296_intervalo.df
// de um painel se atualizar, e por isso precisa de duas coisas: um
// teto opcional de batidas, e um cancelamento que PARA de verdade.
//
// Os testes esperam por CONDICAO, e nao por relogio: um 'sleep' fixo
// mede a maquina, e o mesmo exercicio reprova numa esteira carregada
// sem que nada tenha mudado no codigo.

adopt Arcane.Reativo as R
adopt Arcane.Time as Tempo

steady PASSO := 0.05
steady PRAZO := 5.0

action esperar_ate(condicao):
    "Espera a condicao virar verdadeira, ou desiste no prazo."
    limite := Tempo.timestamp() + PRAZO
    persist Tempo.timestamp() smaller limite:
        given condicao():
            yield yes
        Tempo.sleep(PASSO / 5)
    yield no

out "== 1. ele nao comeca sem ninguem escutando =="

silencioso := R.intervalo(PASSO, quantos := 3)
Tempo.sleep(PASSO * 4)
assert silencioso.inscritos() is 0
out "   passou o tempo de tres batidas, e nada correu"

out ""
out "== 2. com teto, ele termina sozinho =="

batidas := []
fim := []
contado := R.intervalo(PASSO, quantos := 3)
contado.inscrever(lambda n => batidas.append(n), void,
    lambda => fim.append(yes))

assert esperar_ate(lambda => len(fim) is 1)

// A contagem comeca em ZERO, como o 'interval' de todo framework
// reativo: o numero e "quantas ja passaram", e nao "a quantidade".
assert batidas is [0, 1, 2]
out $"   {batidas}"

out ""
out "== 3. sem teto, quem para e o cancelamento =="

livres := []
sempre := R.intervalo(PASSO)
inscricao := sempre.inscrever(lambda n => livres.append(n))

assert esperar_ate(lambda => len(livres) bigger_eq 2)
inscricao.cancelar()
quantas := len(livres)

// Depois de cancelar, o numero nao anda mais — e para PROVAR isso a
// espera precisa ser maior que varias batidas.
Tempo.sleep(PASSO * 5)
assert len(livres) is quantas
out "   cancelada, ela parou de bater"

out ""
out "== 4. o operador vale aqui tambem =="

pares := []
so_pares := R.intervalo(PASSO, quantos := 6).sift(lambda n => n % 2 is 0)
so_pares.inscrever(lambda n => pares.append(n))

assert esperar_ate(lambda => len(pares) is 3)
assert pares is [0, 2, 4]

out "exercicio 296 ok"

intervalo é uma fonte fria com relógio: a forma de um painel se atualizar.

Ele não começa sem ninguém escutando#

Um temporizador que roda sem assinante é trabalho jogado fora — e numa thread que ninguém observa.

Com teto, ele termina sozinho#

E avisa o fim. A contagem começa em zero, como o interval de todo framework reativo: o número é "quantas já passaram", e não "a quantidade".

Sem teto, quem para é o cancelamento#

E ele para de verdade: a prova é o número não andar mais depois de uma espera maior que várias batidas.

297 · quatro formas de lidar com mudanca#

Enunciado. a linguagem tem 'Arcane.Eventos' (um emissor),

exercicios/51-reativo/297_reativo_vs_eventos.df
// 'Arcane.Stream' (topicos com offset), 'stream action' (um gerador
// preguicoso) e agora o reativo. Elas nao resolvem o mesmo problema, e
// escolher a errada e de onde vem metade da complicacao.

adopt Arcane.Reativo as R
adopt Arcane.Eventos as Ev

out "== 1. o emissor: quem escuta recebe o que foi emitido =="

emissor := Ev.emissor()
avisados := []
emissor.ao("pedido.pago", lambda dados => avisados.append(dados["id"]))
emissor.emitir("pedido.pago", {"id": "P-1"})
assert avisados is ["P-1"]

// Pergunta que ele NAO responde: "qual foi o ultimo pedido pago?"
// Ele nao guarda estado.

out ""
out "== 2. o sinal: ele TEM um valor agora =="

ultimo := R.sinal("nenhum")
ultimo.escrever("P-1")
assert ultimo.ler() is "P-1"
out "   a pergunta 'qual e o valor agora?' tem resposta"

out ""
out "== 3. o gerador preguicoso produz sob demanda =="

stream action naturais():
    n := 1
    persist yes:
        emit n
        n += 1

assert naturais().take(4) is [1, 2, 3, 4]
// Quem manda e quem CONSOME. Ninguem e avisado de nada.

out ""
out "== 4. o observavel: um fluxo no tempo, empurrado =="

cliques := R.observavel("cliques")
contados := []
cliques.inscrever(lambda c => contados.append(c))
cliques.emitir("a")
cliques.emitir("b")
assert contados is ["a", "b"]
// Quem manda e quem PRODUZ — e nao ha "valor atual".

out ""
out "== 5. e o derivado e o que nenhum dos outros da =="

// "Este total e sempre a soma daqueles tres" — sem callback, sem
// assinatura, sem recalcular a mao.
a := R.sinal(1)
b := R.sinal(2)
c := R.sinal(3)
steady soma := R.derivado(lambda => a.ler() + b.ler() + c.ler())

assert soma.ler() is 6
b.escrever(20)
assert soma.ler() is 24
out "   ninguem recalculou nada"

out ""
out "== 6. e a ponte entre os dois mundos =="

// Um fluxo vira valor com 'para_sinal'; um valor vira fluxo com
// 'observar'.
fluxo_para_valor := cliques.para_sinal("nenhum")
cliques.emitir("c")
assert fluxo_para_valor.ler() is "c"

do_valor := []
a.observar(lambda v => do_valor.append(v))
a.escrever(9)
assert do_valor is [9]

out "exercicio 297 ok"

A linguagem tem Arcane.Eventos (um emissor), Arcane.Stream (tópicos com offset), stream action (um gerador preguiçoso) e o reativo. Elas não resolvem o mesmo problema, e escolher a errada é de onde vem metade da complicação.

O emissor não guarda estado#

A pergunta "qual foi o último pedido pago?" não tem resposta nele.

O gerador é puxado; o observável é empurrado#

No primeiro, quem manda é quem consome. No segundo, quem produz.

E o derivado é o que nenhum dos outros dá#

"Este total é sempre a soma daqueles três" — sem callback, sem assinatura, sem recalcular à mão.

As duas pontes#

para_sinal leva o fluxo para o mundo dos valores; observar leva o valor para o mundo dos fluxos.


Rode um isolado com dataforge run exercicios/51-reativo/283_sinal_e_derivado.df.