FizzBuzz en TypeScript épisode 3 : résolution

Dans l'épisode précédent, nous avons ajouté la mémoïsation à nos listes infinies en TypeScript.

Dans cet épisode, nous allons résoudre le problème du FizzBuzz.

Rappel du FizzBuzz

Le FizzBuzz est un jeu qui consiste à compter en remplaçant les multiples de 3 par "Fizz", de 5 par "Buzz" et ceux de 3 et 5 par "FizzBuzz".

C'est un des Kata les plus simples pour commencer à pratiquer Test-Driven Development (TDD).

Habituellement, les résolutions se font à base de conditions avec des if pour le cas du Fizz, du Buzz et du FizzBuzz, ce qui donne :

const multiple =
  (by: number) =>
  (number: number): boolean =>
    number % by === 0;

const isFizz = multiple(3);

const isBuzz = multiple(5);

const isFizzBuzz = (number: number): boolean => isFizz(number) && isBuzz(number);

const fizzBuzz = (number: number): string => {
  if (isFizzBuzz(number)) {
    return 'FizzBuzz';
  }
  if (isFizz(number)) {
    return 'Fizz';
  }
  if (isBuzz(number)) {
    return 'Buzz';
  }
  return number.toString();
};

Cependant, il est possible de le résoudre sans ces structures de contrôles comme j'en ai parlé dans le premier épisode via l'article de Gautier.

Résolution

Maintenant que notre structure de List permet de représenter un Range, un Cycle ainsi que des opérations comme Zip et Map, nous allons pouvoir résoudre le FizzBuzz de la même manière :

Pour représenter la liste de tous les nombres jusqu'à l'infini :

const numbers = List.range(1, 1).map((value) => value.toString());

Pour les multiples de Fizz et Buzz, il est nécessaire de représenter la présence ou l'absence d'un élément avec :

const Absent = Symbol('Absent');

type Maybe<T> = T | typeof Absent;

La représentation de tous les cas pour Fizz peut se faire avec un Cycle :

const fizzs = List.cycle<Maybe<string>>(Absent, Absent, 'Fizz');

Et pour Buzz :

const buzzs = List.cycle<Maybe<string>>(Absent, Absent, Absent, Absent, 'Buzz');

Pour avec les opérations sur le Maybe comme :

const isNothing = <T>(value: Maybe<T>): value is typeof Absent => value === Absent;
const isJust = <T>(value: Maybe<T>): value is T => value !== Absent;

Il est possible de définir comment vont fusionner nos Maybe issus des listes fizzs et buzzs un à un :

const stringConcat = (first: Maybe<string>, second: Maybe<string>): Maybe<string> => {
  switch (true) {
    // Lorsqu'on a les deux valeurs à la fois, on les fusionne
    // Exemple: pour "Fizz" et "Buzz", on obtient "FizzBuzz"
    case isJust(first) && isJust(second):
      return [first, second].join('');
    // Lorsqu'on a seulement la première valeur, on la garde
    // Exemple: pour "Fizz" et Absent, on garde "Fizz"
    case isJust(first):
      return first;
    // Lorsqu'on a seulement la seconde valeur, on la garde
    // Exemple: pour Absent et "Buzz", on garde "Buzz"
    case isJust(second):
      return second;
    // Pour tout le reste, on laisse Absent
    default:
      return Absent;
  }
};

Ainsi on peut appliquer notre opération via un Zip pour fusionner les valeurs des listes fizzs et buzzs une à une :

const fizzBuzzZip = fizzs.zipWith(stringConcat, buzzs);

On a donc une liste infinie avec tous les "Fizz", les "Buzz" et les "FizzBuzz" mais pas les nombres (ils sont Absent pour le moment).

Avec l'opération suivante :

const fromMaybe = <T>(left: Maybe<T>, right: Maybe<T>): Maybe<T> => ([left, right].find(isJust) || Absent) as Maybe<T>;

On peut fusionner notre liste avec les nombres :

const fizzBuzzStream = fizzBuzzZip.zipWith(fromMaybe, numbers);

Le fizzBuzzStream est donc une liste infinie avec les "Fizz", les "Buzz", les "FizzBuzz" ainsi que les nombres.

Il ne nous reste plus qu'à chercher notre valeur en parcourant cette liste via son index :

export const fizzBuzz = (number: number) => fizzBuzzStream.at(number - 1);

Pour rappel, l'index commence à 0, d'où le - 1.

La fonction fizzBuzz a donc le même comportement que la première version avec des if.

Fin de l'épisode

Nous venons de résoudre le FizzBuzz de manière fonctionnelle en utilisant notre structure de List infinie qui a été définie dans les articles précédents.

À bientôt pour l'épisode bonus qui donnera une autre représentation des listes infinies en TypeScript.