Percursos
/
Factor
Factor
/
Exercícios
/
O Livro-Razão da Bibliotecária
O Livro-Razão da Bibliotecária

O Livro-Razão da Bibliotecária

Exercício de aprendizagem

Introdução

Às vezes queres combinar uma sequência num único valor; outras vezes queres ver todos os valores intermédios que a combinação produz pelo caminho. O Factor divide isto em duas ferramentas: reduce (em sequences) para a redução a um único valor, e a família cumulativa em math.statistics para a forma acumulada.

reduce: a redução geral

reduce ( seq init quot: ( prev elt -- next ) -- result )

reduce percorre uma sequência um item de cada vez, levando consigo um resultado acumulado (o acumulador) e alimentando com ele uma quotation de dois argumentos. A quotation recebe o valor acumulado e o elemento seguinte; o que quer que deixe na pilha passa a ser o novo acumulador.

USING: math sequences ;

{ 1 2 3 4 } 0 [ + ] reduce .         ! => 10
{ 1 2 3 4 } 1 [ * ] reduce .         ! => 24

Uma semente diferente de zero e um combinador personalizado são as partes de reduce a que sum e product não chegam. Por exemplo, o maior valor de uma sequência, com um valor predefinido caso nenhum valor o supere:

USING: math.order ;

{ 3 1 -4 5 -2 } 0 [ max ] reduce .   ! => 5
{ -3 -1 -4 }    0 [ max ] reduce .   ! => 0

A semente 0 participa na comparação: funciona como resultado quando todos os elementos perdem, pelo que uma sequência só de valores negativos produz na mesma 0, e não um valor mínimo arbitrário.

Reduções cumulativas

Às vezes queres todos os resultados intermédios, não apenas o final. A família cumulativa em math.statistics devolve uma sequência com o mesmo comprimento que a entrada, em que cada posição é a redução sobre o prefixo que termina nessa posição:

cum-sum     ( seq -- newseq )    ! running total
cum-product ( seq -- newseq )    ! running product
cum-min     ( seq -- newseq )    ! running minimum
cum-max     ( seq -- newseq )    ! running maximum
USING: math.statistics ;

{ 3 1 4 1 5 9 2 6 } cum-sum .        ! => { 3 4 8 9 14 23 25 31 }
{ 1 2 3 4 } cum-product .            ! => { 1 2 6 24 }
{ 3 1 4 1 5 9 2 6 } cum-min .        ! => { 3 1 1 1 1 1 1 1 }
{ 3 1 4 1 5 9 2 6 } cum-max .        ! => { 3 3 4 4 5 9 9 9 }

Um padrão útil são as reduções cumulativas encadeadas: a saída de uma é ela própria uma sequência, pronta a alimentar outra. Isso torna o "resumo acumulado de um resumo acumulado" exprimível em duas palavras. As combinações são flexíveis: junta-as aos pares com base no que cada passo está a resumir.

produce: o desdobramento

reduce consome uma sequência e produz um valor. produce (em sequences) segue o caminho inverso: gera uma sequência a partir de uma semente, testando e avançando repetidamente:

produce ( pred quot -- seq )

Cada iteração começa por executar pred sobre o estado atual; se devolver um valor verdadeiro, quot é chamada para produzir o elemento seguinte e atualizar o estado. Quando pred devolve f, a iteração para e os elementos reunidos são devolvidos.

Um exemplo clássico é a sequência de Fibonacci (cada número é a soma dos dois anteriores). O estado acumulado é o par (a, b). Cada passo emite b e substitui depois o par por (b, a + b):

USING: kernel math sequences ;

! Fibonacci numbers strictly below 100:
0 1 [ dup 100 < ] [ tuck + over ] produce 2nip .
! => { 1 1 2 3 5 8 13 21 34 55 89 }

O estado acumulado abrange dois valores, pelo que o corpo usa tuck (em kernel), a reorganização de três elementos que copia o topo para baixo do segundo, para avançar o par, e 2nip (também em kernel, o análogo de dois elementos de nip) para arrumar tudo no fim. Lendo a chamada da esquerda para a direita:

  • O predicado [ dup 100 < ] espreita o topo do par (o próximo número a emitir) e continua enquanto este ainda estiver abaixo do limite.
  • O corpo [ tuck + over ] avança o estado para (b, a + b) e emite b, deixando três valores na pilha: o novo par em baixo e o número emitido no topo.
  • Depois de produce parar, os dois valores que ficam no fim (o par final) são descartados com 2nip, deixando apenas a sequência produzida.

produce é o dual exato de reduce: onde reduce reduz uma sequência a um valor, produce desdobra um valor numa sequência.

Instruções

Trabalhas na biblioteca e manténs o livro de contas dos utentes. Todas as semanas, chegam à tua secretária dois tipos de trabalho:

  • Uma fila de pedidos: créditos que um utente pede para aplicar (devoluções de livros, multas pagas) e novos débitos registados pelo sistema (multas por atraso entretanto acumuladas). A conta do utente está protegida contra saldos negativos: um crédito suficientemente grande para deixar o utente no vermelho é aplicado apenas até ao valor em dívida, pelo que o saldo acumulado nunca desce abaixo de zero.
  • Uma lista de transações: registos já lançados na conta. Os valores positivos são débitos (novas multas) e os valores negativos são créditos (pagamentos).

Todas as semanas fazes o balanço: um saldo final depois de atenderes os pedidos, um saldo acumulado por dia a partir das transações e um valor mínimo acumulado que assinala os períodos em que as multas dispararam.

1. Atende a fila de pedidos

Define protected-balance de modo a que receba um saldo opening e um array de requests (valores com sinal) e devolva o saldo final depois de atender cada pedido pela ordem. Um levantamento que faria o saldo descer abaixo de zero é atendido apenas até ao valor disponível, pelo que o saldo acumulado nunca desce abaixo de zero.

100 { 50 -200 30 } protected-balance .
! => 30

500 { 100 -300 -250 } protected-balance .
! => 50

0 { -10 50 } protected-balance .
! => 50

2. Saldo acumulado

Define running-balance de modo a que receba um array de transactions e devolva uma sequência com o mesmo comprimento, cujo elemento de índice i é o saldo após as primeiras i+1 transações (em relação a um saldo inicial de zero).

{ 50 -30 -20 100 } running-balance .
! => { 50 20 0 100 }

3. Saldo mínimo até ao momento

Define least-balance-so-far de modo a que receba um array de transactions e devolva uma sequência com o mesmo comprimento, cujo elemento de índice i é o saldo acumulado mais baixo observado até à posição i, inclusive. Este é o mínimo acumulado, útil para identificar os dias em que a conta pareceu arriscada.

{ 50 -30 -20 100 } least-balance-so-far .
! => { 50 20 0 0 }

{ 200 -50 -100 -200 } least-balance-so-far .
! => { 200 150 50 -150 }

4. Reduz para metade até ao alvo

A biblioteca tem um programa de amnistia de multas: o saldo em dívida de um utente é reduzido para metade em cada período de pagamento, até ficar igual ou abaixo de um limiar de perdão. Define halve-until de modo a que receba um principal e um target e devolva a sequência de valores reduzidos para metade (usando divisão inteira), começando na primeira redução para metade e continuando enquanto o valor corrente for estritamente superior a target. O último valor emitido será o primeiro que fica igual ou abaixo de target.

100 5 halve-until .
! => { 50 25 12 6 3 }

64 1 halve-until .
! => { 32 16 8 4 2 1 }

3 5 halve-until .
! => { }
Editar via GitHub A ligação abre numa nova janela ou separador
Factor Exercism

Estás pronto para começar O Livro-Razão da Bibliotecária?

Inscreve-te no Exercism para aprenderes e dominares Factor com 47 conceitos163 exercícios, e mentoria humana real, tudo grátis.