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éralreduce ( 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.
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 :
[ dup 100 < ] regarde le sommet de la paire (le prochain nombre à émettre) et continue tant qu'il reste en dessous de la borne.[ 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.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.
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 :
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é.
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
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 }
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 }
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 .
! => { }
Inscris-toi sur Exercism pour apprendre et maîtriser Factor avec 47 concepts163 exercices, et un vrai mentorat humain, le tout gratuitement.