Parcours
/
Factor
Factor
/
Exercices
/
Le registre du bibliothécaire
Le registre du bibliothécaire

Le registre du bibliothécaire

Exercice d'apprentissage

Introduction

Il arrive qu'on veuille combiner une séquence en une seule valeur ; il arrive aussi qu'on veuille voir chaque valeur intermédiaire produite par la combinaison en cours de route. Factor répartit cela en deux outils : reduce (dans sequences) pour le fold à valeur unique, et la famille cumulative de math.statistics pour la variante progressive.

reduce : le fold général

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

reduce parcourt une séquence élément par élément, en transportant un résultat courant (l'accumulateur) qu'il fournit à une quotation à deux arguments. La quotation reçoit l'accumulateur courant et l'élément suivant ; ce qu'elle laisse sur la pile devient le nouvel accumulateur.

USING: math sequences ;

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

Une valeur initiale non nulle et un combinateur personnalisé sont ce que sum et product ne peuvent pas atteindre. Par exemple, la plus grande valeur d'une séquence, avec une valeur par défaut si aucune valeur ne la dépasse :

USING: math.order ;

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

La valeur initiale 0 participe à la comparaison : elle sert de résultat lorsque tous les éléments perdent, si bien qu'une séquence de valeurs toutes négatives produit quand même 0 au lieu d'une plus petite valeur arbitraire.

Les réductions cumulatives

Parfois, on veut chaque résultat intermédiaire, pas seulement le dernier. La famille cumulative de math.statistics renvoie une séquence de même longueur que l'entrée, où chaque position correspond à la réduction du préfixe qui se termine à cette position :

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 }

Un schéma utile consiste en des réductions cumulatives enchaînées : la sortie de l'une est elle-même une séquence, prête à alimenter la suivante. On peut ainsi exprimer « le résumé courant d'un résumé courant » en deux mots. Les combinaisons sont souples : associe-les selon ce que chaque étape résume.

produce : le unfold

reduce consomme une séquence pour produire une valeur. produce (dans sequences) va dans l'autre sens : il génère une séquence à partir d'une valeur initiale en testant et en avançant de façon répétée :

produce ( pred quot -- seq )

À chaque itération, pred est d'abord exécuté sur l'état courant ; s'il renvoie une valeur vraie, quot est appelé pour produire l'élément suivant et mettre à jour l'état. Quand pred renvoie f, l'itération s'arrête et les éléments collectés sont renvoyés.

Un exemple classique est la suite de Fibonacci (chaque nombre est la somme des deux précédents). L'état courant est la paire (a, b). Chaque étape émet b, puis remplace la paire par (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 }

L'état courant couvre deux valeurs, donc le corps utilise tuck (dans kernel), le réarrangement à trois éléments qui copie le sommet sous le deuxième, pour faire avancer la paire, et 2nip (également dans kernel, l'analogue à deux éléments de nip) pour nettoyer la pile à la fin. Lisons l'appel de gauche à droite :

  • Le prédicat [ dup 100 < ] regarde le sommet de la paire (le prochain nombre à émettre) et continue tant qu'il reste en dessous de la borne.
  • Le corps [ tuck + over ] fait passer l'état à (b, a + b) et émet b, laissant trois valeurs sur la pile : la nouvelle paire en dessous, le nombre émis au sommet.
  • Une fois que produce s'arrête, les deux valeurs restantes (la paire finale) sont écartées avec 2nip, ce qui ne laisse que la séquence produite.

produce est le dual exact de reduce : là où reduce plie une séquence en une valeur, produce déplie une valeur en une séquence.

Instructions

Tu es le bibliothécaire, chargé de tenir le registre des comptes des abonnés. Chaque semaine, deux types de tâches arrivent sur ton bureau :

  • Une file de requêtes : des crédits qu'un abonné demande à appliquer (retours de livres, amendes payées) et de nouveaux débits que le système a enregistrés (amendes de retard qui viennent de s'accumuler). Le compte de l'abonné est protégé au crédit : un crédit suffisamment important pour faire passer l'abonné dans le rouge n'est appliqué qu'à hauteur de ce qui est dû, si bien que le solde courant ne descend jamais en dessous de zéro.
  • Une liste de transactions : des écritures déjà enregistrées sur le compte. Les montants positifs sont des débits (nouvelles amendes), les montants négatifs sont des crédits (paiements).

Chaque semaine, tu fais les comptes : un solde final après avoir honoré les requêtes, un solde courant par jour à partir des transactions, et un plus bas niveau courant pour signaler les périodes où les amendes ont grimpé.

1. Honore la file de requêtes

Définis protected-balance pour qu'il prenne un solde opening et un tableau de requests (montants signés), et qu'il renvoie le solde final après avoir honoré chaque requête l'une après l'autre. Un retrait qui ferait passer le solde sous zéro n'est honoré qu'à hauteur du montant disponible, si bien que le solde courant ne peut pas passer sous zéro.

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

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

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

2. Solde courant

Définis running-balance pour qu'il prenne un tableau de transactions et renvoie une séquence de même longueur dont le i-ième élément est le solde après les i+1 premières transactions (par rapport à un solde de départ nul).

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

3. Plus bas solde jusqu'ici

Définis least-balance-so-far pour qu'il prenne un tableau de transactions et renvoie une séquence de même longueur dont le i-ième élément est le solde courant le plus bas observé jusqu'à la position i incluse. C'est le plus bas niveau courant, utile pour repérer les jours où le compte semblait risqué.

{ 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. Divise par deux jusqu'à la cible

La bibliothèque lance un programme d'amnistie des amendes : le solde impayé d'un abonné est divisé par deux à chaque période de paiement jusqu'à ce qu'il atteigne un seuil de remise ou passe en dessous. Définis halve-until pour qu'il prenne un principal et un target, et renvoie la séquence des valeurs divisées par deux (en utilisant la division entière), en partant de la première division et en continuant tant que la valeur courante reste strictement supérieure à target. La dernière valeur émise sera la première à tomber à target ou en dessous.

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

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

3 5 halve-until .
! => { }
Modifie via GitHub Le lien s'ouvre dans une nouvelle fenêtre ou un nouvel onglet
Factor Exercism

Prêt à commencer Le registre du bibliothécaire ?

Inscris-toi sur Exercism pour apprendre et maîtriser Factor avec 47 concepts163 exercices, et un vrai mentorat humain, le tout gratuitement.