_Y S _ b l o g _

【TypeScript】FizzBuzzのいろんな書き方

FizzBuzzとは

FizzBuzzは、プログラミングの練習問題として有名で、次のようなルールで1から順番に整数を出力するプログラムです。

・3の倍数ではなく、5の倍数でもないときは整数をそのまま出力する
・3の倍数であり、5の倍数でない時は整数の代わりにFizzと出力する
・3の倍数でなく、5の倍数であるときは整数の代わりにBuzzと出力する
・3の倍数であり、5の倍数でもある時は整数の代わりにFizzBuzzと出力する

例えば、1から15までFizzBuzzを行うと次のように出力されます。

1
2
Fizz
4
Buzz
Fizz
7
8
Fizz
Buzz
11
Fizz
13
14
FizzBuzz

今回は、このプログラムのいろいろな書き方を考えてみたいと思います。

パターン1:普通

for(let num = 1; num <= 15; num++) {
    if (num % 3 === 0 && num % 5 === 0) console.log('FizzBuzz');
    if (num % 5 === 0) console.log('Buzz');
    if (num % 3 === 0) console.log('Fizz');

    console.log(num.toString());
}

ちなみに、num % 3 === 0 && num % 5 === 0の部分はnum % 15 === 0でもOKですが、私はnum % 3 === 0 && num % 5 === 0の方が良いかと思います。

理由としては、単純に分かりやすいからです。

今回は35なのでどちらでも良かったですが、例えば問題(要件)に変更が入り68になったとします。
すると、num % 6 === 0 && num % 8 === 0num % 24 === 0になりますね。
この24という数字は、68の最小公倍数をとっているだけなのですが、パッとみただけではわからないかもしれません。

よって、分かりやすさとメンテナンス性からnum % 3 === 0 && num % 5 === 0が良いと思います。

パターン2:ループの中の処理を関数に抜き出す

const getFizzBuzz = (num: number): string => {
    if (num % 3 === 0 && num % 5 === 0) return 'FizzBuzz';
    if (num % 5 === 0) return 'Buzz';
    if (num % 3 === 0) return 'Fizz';

    return num.toString();
};

for(let num = 1; num <= 15; num++) {
    console.log(getFizzBuzz(num));
}

ループの中の処理をgetFizzBuzzという関数として抜き出しました。

このように大きな処理の一部を関数に抜き出すことは、以下の2つ理由からおすすめします。

1.処理に名前がつく

関数として抜き出すことにより、ループの中の処理にgetFizzBuzzという名前がつきました。
関数や変数の名前には、中身を説明するという役割があります。
ですので、この関数はnumという数字を受け取ってFizzBuzzの結果をgetする関数なんだなあと、中身を詳しく読まなくても把握できます。

2.型情報が付けられる

関数として抜き出すことにより、ループの中の処理に型情報をつけることができました。
このように関数の引数・返り値の方を明記することで、プログラムの安全性が向上するのはもちろんのこと、関数の処理の流れ・データの流れがわかりやすくなります。

パターン3:文字列として連結させる

const getFizzBuzz = (num: number): string => {
    let result = '';
    if (num % 3 === 0) result += 'Fizz';
    if (num % 5 === 0) result += 'Buzz';
    return result || num.toString();
};

for (let num = 1; num <= 15; num++) {
    console.log(getFizzBuzz(num));
};

空の文字列を作り、そこに条件分岐で当てはまる場合にFizzやBuzzを連結させていくという書き方です。

この書き方の良いところは、以下の2つです。

1.条件分岐が1つ減る

パターン2までは必要だったif(num % 3 === 0 && num % 5 === 0) { return 'FizzBuzz'; }の部分が省略できるところです。
最初はresult=''で、3の倍数だったらresult='Fizz'になり、さらに5の倍数でもあればresult='FizzBuzz'になるというように、どんどん連結されていくのですね。

2.メンテナンス性に優れている

if(num % 3 === 0 && num % 5 === 0) { return 'FizzBuzz'; }という文がなくなったことにより、問題(要件)が変わった場合に修正の必要な箇所が格段に減りました。
(例えば、という数字がに変わり、FizzBuzzJizzDuzzに変わった場合を考えてみてください。)

また、return result || num.toString();という部分で、短絡演算を使用しているところもポイントです。
この部分を短絡演算を使用せずに書くと次のように冗長になります。

const getFizzBuzz = (num: number): string => {
    let result = '';
    if (num % 3 === 0) result += 'Fizz';
    if (num % 5 === 0) result += 'Buzz';

    if (result) {
        return result;
    } else {
        return num.toString();
    };
};

for (let num = 1; num <= 15; num++) {
    console.log(getFizzBuzz(num));
};

パターン4:テンプレートリテラルと三項演算子を使用する

const getFizzBuzz = (num: number): string => {
    return `${num % 3 === 0 ? 'Fizz' : ''}${num % 5 === 0 ? 'Buzz' : ''}` || num.toString();
};

for (let num = 1; num <= 15; num++) {
    console.log(getFizzBuzz(num));
};

この書き方の良いところは、関数内の処理がたったの1行で書けているところです。

また、テンプレートリテラルの中で三項演算子を用いているところがポイントです。
三項演算子については、こちらの記事をご覧ください。

まとめ

他にも、以下のように高階関数を使って書くこともできますが、コードが難しくなるだけで他の書き方と比べて特段のメリットがあるわけでもないのでオススメしません。

const fizzBuzz = (num: number, rules: Array<(n: number) => string>): string => {
    for (const rule of rules) {
        const result = rule(num);
        if (result) return result;
    }
    return num.toString();
};

const rules = [
    (n: number) => (n % 15 === 0 ? 'FizzBuzz' : ''),
    (n: number) => (n % 3 === 0 ? 'Fizz' : ''),
    (n: number) => (n % 5 === 0 ? 'Buzz' : '')
];

for (let num = 1; num <= 15; num++) {
    console.log(fizzBuzz(num, rules));
};

実用性があるかどうかはさておき、TypeScriptの勉強になるのでいろいろな書き方を試してみると面白いかもしれません。

他にもっといい書き方があるよという方はぜひ教えてください!