⭠ Retour
FizzBuzz sans modulo ? Discutable...
🗓️ 06/05/2026 🕜 6min
ruby
FizzBuzz, c'est le rituel de passage de tous les développeurs ou presque quand ils débutent. Le principe est un jeu d'enfant : parcourir une suite d’entiers, remplacer les multiples de trois par Fizz, les multiples de cinq par Buzz, et les multiples communs par FizzBuzz. Loin de faire de vous un mathématicien rivalisant avec Einstein, cet exercice a pour but de prouver les bonne utilisation des opérateurs conditionnelles dans un langage.
La solution attendue relève essentiellement de divisions pour chaque entier à l'aide de l'opérateur modulo, on arrive assez aisément à une solution concise, expressive et compréhensible comme celle présenté par Jeremy Gagon.
Mais cet article aura pour but d'étudier une solution un peu plus archaïque.
Ruby est connu pour être un langage élégant, qui privilégie la clarté, mais il subsiste des mécanismes presque oubliés comme l'opérateur flip-flop (ceux qui ont des bases en électronique ne doivent pas être étranger au terme).
Hérité du langage Perl, il est peu utilisé de nos jours, peu enseigné dans les cursus Ruby, voire inconnu des développeurs expérimentés, mais il existe ! Cet opérateur ne se contente pas d'évaluer une condition de manière instantané, il conserve un état interne entre deux évaluations successives.
Ce n'est donc pas un simple test booléen, c'est plus un petit automate logique qui oscille entre l'état fermé et ouvert.
Et c'est ainsi qu'on se retrouve avec le code ci-dessous qui ressemble plus à du code pour ouvrir la Porte des Étoiles qu'à du code Ruby:
La solution attendue relève essentiellement de divisions pour chaque entier à l'aide de l'opérateur modulo, on arrive assez aisément à une solution concise, expressive et compréhensible comme celle présenté par Jeremy Gagon.
Mais cet article aura pour but d'étudier une solution un peu plus archaïque.
Ruby est connu pour être un langage élégant, qui privilégie la clarté, mais il subsiste des mécanismes presque oubliés comme l'opérateur flip-flop (ceux qui ont des bases en électronique ne doivent pas être étranger au terme).
Hérité du langage Perl, il est peu utilisé de nos jours, peu enseigné dans les cursus Ruby, voire inconnu des développeurs expérimentés, mais il existe ! Cet opérateur ne se contente pas d'évaluer une condition de manière instantané, il conserve un état interne entre deux évaluations successives.
Ce n'est donc pas un simple test booléen, c'est plus un petit automate logique qui oscille entre l'état fermé et ouvert.
Et c'est ainsi qu'on se retrouve avec le code ci-dessous qui ressemble plus à du code pour ouvrir la Porte des Étoiles qu'à du code Ruby:
a = b = c = nil (1.100).each do |num| print num, ("Fizz" unless (a = !a) .. (a = !a)), ("Buzz" unless (b = !b) ... !((c = !c) .. (c = !c))), "\n" end
Aucun modulo. Aucune division. Aucune structure conditionnelle.
Ici, on utilise le principe des inversions booléenne (!a, !b, !c) ainsi que les deux variante de l'opérateur flip-flop (.. et ...).
Et pourtant ça marche ! Ainsi, la question que tout le monde se pose (ou juste moi...) est la suivante:
Ici, on utilise le principe des inversions booléenne (!a, !b, !c) ainsi que les deux variante de l'opérateur flip-flop (.. et ...).
Et pourtant ça marche ! Ainsi, la question que tout le monde se pose (ou juste moi...) est la suivante:
Comment de simples basculements booléens, sans aucun calcul, parvient-il à faire fonctionner le FizzBuzz ?
Suivez moi ! Plongeons tout d'abord dans le fonctionnement de l'opérateur flip-flop pour comprendre ce qu'il se passe dans ce code.
L'automate binaire
En Ruby, lorsque l'on a une expression de la forme:
condition_1 .. condition_2
Il ne s'agit pas d'une simple comparaison entre deux conditions.
L'idée est qu'elle possède une mémoire interne qui lui permet de conserver son état d'une itération à la suivante. C'est la différence avec une expression booléenne ordinaire.
Et comme en électronique, il est possible de l'utiliser comme un automate à deux états: fermé et ouvert.
Lorsque le flip-flop est fermé, le code n'évalue que la condition_1. Tant que cette condition reste fausse, l'expression entière retourne false et l'opérateur est fermé. Dès que la condition devient vraie, le flip-flop s'ouvre et le fonctionnement change.
Une fois ouvert, l'opérateur retourne true. A partir de ce moment là, le code n'évalue plus la condition_1 et se concentre uniquement sur la condition_2. Tant que la condition_2 reste fausse, le flip-flop reste ouvert et retourne true. Ce n'est que lorsque condition_2 devient vraie que l'opérateur se referme et que le cycle peut recommencer.
Mal de crâne ? Moi aussi...
Il faut bien différencier la condition booléenne classique de l'opérateur flip-flop. Ici, le résultat dépend des conditions au moment de l'évaluation, mais aussi de l'état obtenu lors de l'évaluation précédente.
L'idée est qu'elle possède une mémoire interne qui lui permet de conserver son état d'une itération à la suivante. C'est la différence avec une expression booléenne ordinaire.
Et comme en électronique, il est possible de l'utiliser comme un automate à deux états: fermé et ouvert.
Lorsque le flip-flop est fermé, le code n'évalue que la condition_1. Tant que cette condition reste fausse, l'expression entière retourne false et l'opérateur est fermé. Dès que la condition devient vraie, le flip-flop s'ouvre et le fonctionnement change.
Une fois ouvert, l'opérateur retourne true. A partir de ce moment là, le code n'évalue plus la condition_1 et se concentre uniquement sur la condition_2. Tant que la condition_2 reste fausse, le flip-flop reste ouvert et retourne true. Ce n'est que lorsque condition_2 devient vraie que l'opérateur se referme et que le cycle peut recommencer.
Mal de crâne ? Moi aussi...
Il faut bien différencier la condition booléenne classique de l'opérateur flip-flop. Ici, le résultat dépend des conditions au moment de l'évaluation, mais aussi de l'état obtenu lors de l'évaluation précédente.
Dans quel état suis-je au moment où cette condition est évaluée ?
Cette question à laquelle répond l'opérateur flip-flop est la différence cruciale.
C'est celle-ci qui permet de créer la dynamique de séquence entre true et false, on est sur un rythme logique: vous pouvez vous imaginer une petite porte qui s'ouvre et se ferme en fonction du résultat de l'expression.
Suivez moi ! Revenons à notre code FizzBuzz maintenant que nous sommes des experts.
C'est celle-ci qui permet de créer la dynamique de séquence entre true et false, on est sur un rythme logique: vous pouvez vous imaginer une petite porte qui s'ouvre et se ferme en fonction du résultat de l'expression.
Suivez moi ! Revenons à notre code FizzBuzz maintenant que nous sommes des experts.
Génération de Fizz
(a = !a) .. (a = !a)
Attends ? C'est normal que les deux côtés de l'expression soient identiques ? Oui. C'est parce que chaque côté n'intervient pas au même moment. le flip-flop n'évalue jamais les deux conditions simultanément, il n'en évalue qu'une seule selon l'état précédent sauvegardé dans sa mémoire interne.
la variable a est initialisé à nil.
nil est false en Ruby. Ainsi, la première opération !a est égale à true et assigné à la variable a.
Le flip-flop s'ouvre sur cette première itération (le chiffre 1 dans le range 1.100).
Sur la deuxième itération (le chiffre 2), Ruby évalue la condition de fermeture (a = !a).
Cette fois a = true et !a est donc égale à false puis réassigné à la variable a.
Le flip-flop se referme, mais cette deuxième itération retourne true.
Lors de la troisième itération (le chiffre 3), c'est la condition d'ouverture qui est une nouvelle fois évalué.
La variable a vaut false désormais, l'expression retourne true puis inverse de nouveau l'état de la variable a.
Cependant, c'est la première fois que l'itération retourne false !
(Moi j'ai pris un doliprane les gars là...)
En résumé, ce code produit le cycle suivant et recommence en suivant le même motif:
la variable a est initialisé à nil.
nil est false en Ruby. Ainsi, la première opération !a est égale à true et assigné à la variable a.
Le flip-flop s'ouvre sur cette première itération (le chiffre 1 dans le range 1.100).
Sur la deuxième itération (le chiffre 2), Ruby évalue la condition de fermeture (a = !a).
Cette fois a = true et !a est donc égale à false puis réassigné à la variable a.
Le flip-flop se referme, mais cette deuxième itération retourne true.
Lors de la troisième itération (le chiffre 3), c'est la condition d'ouverture qui est une nouvelle fois évalué.
La variable a vaut false désormais, l'expression retourne true puis inverse de nouveau l'état de la variable a.
Cependant, c'est la première fois que l'itération retourne false !
(Moi j'ai pris un doliprane les gars là...)
En résumé, ce code produit le cycle suivant et recommence en suivant le même motif:
- flip-flop ouvert / true
- flip-flop fermé / true
- flip-flop fermé / false
Dans le programme complet, le code est utilisé de la manière suivante:
("Fizz" unless (a = !a) .. (a = !a))
Le mot "Fizz" est affiché seulement quand l'expression retourne false.
Et ce false, on l'a toutes les trois évaluations.
On aura donc Fizz qui s'affiche sur tous les nombres qui sont un multiple de 3.
Et ce false, on l'a toutes les trois évaluations.
On aura donc Fizz qui s'affiche sur tous les nombres qui sont un multiple de 3.