🙆

FizzBuzzを完全な関数プログラミングで書いてみた

に公開

typescriptのブルーベリー本を読んでいて、王道のFizzBuzz問題に出会った。これを手続型プログラミングとなんちゃって関数型プログラミングで書いてみる。

一般的な書き方

ブルーベリー本の模範解答でもある。

for (let i = 1; i <= 100; i++) {
  if (i % 3 === 0 && i % 5 === 0) {
    console.log("FizzBuzz");
  } else if (i % 3 === 0) {
    console.log("Fizz");
  } else if (i % 5 === 0) {
    console.log("Buzz");
  } else {
    console.log(i);
  }
}

このコードでは、iというループ変数の状態をforループ内で変更しながら処理を進めている。console.logによる出力もループの中に直接記述されており、処理と副作用が密接に結合している。

なんちゃってFizzBuzz関数

私がブルーベリー本でFizzBuzzを書けと言われて最初に書いたやつ

function FizzBuzz(n: number): string {
  if (n % 3 === 0 && n % 5 === 0) {
    return "FizzBuzz";
  }
  if (n % 3 === 0) {
    return "Fizz";
  }
  if (n % 5 === 0) {
    return "Buzz";
  } else {
    return n.toString();
  }
}

このFizzBuzz関数は、与えられた数値nに対して適切な文字列を返す。しかし、この関数を実際に利用するためには、やはりforループなどを用いて呼び出す必要があるため、全体としてはまだ手続き型の範疇に留まる。

完全な関数型FizzBuzz

// 関数型の特徴:
// - 純粋関数(同じ入力に対して常に同じ出力を返す)
// - 副作用を分離する
// - イミュータブル(状態を変更しない)
// - 関数の組み合わせで処理を構築する
const range = (start: number, end: number): number[] => 
  Array.from({ length: end - start + 1 }, (_, i) => start + i);

const fizzBuzz = (n: number): string => {
  if (n % 3 === 0 && n % 5 === 0) return "FizzBuzz";
  if (n % 3 === 0) return "Fizz";
  if (n % 5 === 0) return "Buzz";
  return n.toString();
};

// 純粋な関数の部分(副作用なし)
const fizzBuzzSequence = (start: number, end: number): string[] =>
  range(start, end).map(fizzBuzz);

// 副作用を分離
const printFizzBuzz = (start: number, end: number): void => {
  fizzBuzzSequence(start, end).forEach(console.log);
};

手続型プログラミングと関数型プログラミングの主な違いは以下の通り。

状態管理

  • 手続型: ループ変数 i の状態を変更しながら処理を進める。
  • 関数型: 状態の変更を避け、新しい値を作成する(イミュータブル)。

副作用の扱い

  • 手続型: 処理と副作用(console.log)が混在している。
  • 関数型: 純粋な計算部分(fizzBuzzSequence)と副作用(printFizzBuzz)を分離。

関数の性質

  • 手続型: 命令の列としての関数(何をするか)。
  • 関数型: 数学的な関数としての性質(入力から出力への変換)。

コードの再利用性

  • 手続型: ループの中にロジックが埋め込まれている。
  • 関数型: 小さな関数を組み合わせて新しい機能を作れる(range, map, forEach)。

テストのしやすさ

  • 手続型: 副作用を含むため、テストが複雑。
  • 関数型: 純粋関数は入力と出力の関係だけをテストすれば良い。

並行処理

  • 手続型: 状態の共有により並行処理が難しい。
  • 関数型: 状態の変更がないため、並行処理が容易。

ただし、重要なのは「どちらが正しい」ではなく、「どのアプローチが問題に適しているか」である。

例えば:

  • 単純な処理で、パフォーマンスが重要な場合 → 手続型
  • 複雑なデータ変換や、テストが重要な場合 → 関数型
  • 状態管理が複雑な場合 → 関数型
  • メモリ使用量が制限される場合 → 手続型

また、実際のアプリケーションでは、両方のアプローチを組み合わせて使用することも一般的。TypeScriptは両方のパラダイムをサポートしているため、状況に応じて適切なアプローチを選択できる。

Discussion