見出し画像

JavaScriptで理解するcomposeとpipe:コードを簡潔にする関数合成の基本

JavaScriptでよく使用される「compose(コンポーズ)」と「pipe(パイプ)」の概念について解説します。JavaScriptの経験がある方であれば、これらの概念に出会ったことがあるかもしれません。これらの概念は比較的シンプルですが、時として混乱を招くことがあります。それでは、より良い理解のために詳しく見ていきましょう。
まず、これらの類似点について見てみましょう。composeとpipeは、どちらもデータ変換と関数合成において重要な概念です。簡単に言えば、機能を小さな再利用可能なコンポーネントに分割することで複雑さを軽減します。これらの小さな関数を組み合わせることで、より複雑な機能を作成でき、コードの再利用性と可読性が向上します。
composeとpipeは、どちらも複数の関数を引数として受け取り、それらの関数を組み合わせた新しい関数を返します。

compose(コンポーズ)

composeは複数の関数を引数として受け取り、それらの関数の合成である新しい関数を返します。重要なポイントは、関数の実行順序が右から左であることです。つまり、最も右の関数が最初に実行され、その出力が左隣の関数の引数として渡され、以降同様に処理が続きます。

const add2 = (x) => x + 2;
const square = (x) => x * x;
const subtract2 = (x) => x - 2;
const compose = (...functions) => (value) =>
 functions.reduceRight((acc, fn) => fn(acc), value);

const composedResult = compose(subtract2, square, add2);
console.log(composedResult(4));

説明

  1. まず、4がadd2に渡され、結果は6となります。

  2. 次に、6がsquareで二乗され、結果は36となります。

  3. 最後に、subtract2で2が引かれ、最終的な出力は34となります。

pipe(パイプ)

pipeはcomposeと似ていますが、関数の実行順序が左から右になります。これにより、データの流れが関数の順序と一致するため、一部の開発者にとってより直感的です。

const add2 = (x) => x + 2;
const square = (x) => x * x;
const subtract2 = (x) => x - 2;
const pipe = (...functions) => (value) =>
functions.reduce((acc, fn) => fn(acc), value);
const pipedResult = pipe(subtract2, square, add2);
console.log(pipedResult(4));

説明

  1. まず、4がsubtract2に渡され、結果は2となります。

  2. 次に、2がsquareで二乗され、結果は4となります。

  3. 最後に、add2で2が加えられ、最終的な出力は6となります。

composeとpipeを使用した複雑さの軽減
以下のような処理が必要なシステムを想像してみてください:

  1. ユーザー入力の検証

  2. 入力のフォーマット

  3. データベースへの保存

composeやpipeを使用しない場合、これは入れ子になった関数呼び出しのように見えるでしょう。

storeInDB(formatInput(validateInput(userInput)));

composeを使用して改善する例

const validateInput = (input) => {
  if (!input || input.trim() === '') {
    throw new Error('Invalid input');
  }
  return input.trim();
};

const formatInput = (input) => input.toLowerCase();

const storeInDB = (input) => {
  console.log(`Storing "${input}" in the database.`);
  return `Stored: ${input}`;
};

// Compose the steps
const compose = (...functions) => (value) =>
  functions.reduceRight((acc, fn) => fn(acc), value);

const processUserInput = compose(storeInDB, formatInput, validateInput);

console.log(processUserInput('my input'));

同じ処理をpipeで実装する例

const pipe = (...functions) => (value) =>
  functions.reduce((acc, fn) => fn(acc), value);

const processUserInputWithPipe = pipe(validateInput, formatInput, storeInDB);

console.log(processUserInputWithPipe('my input'));

もう一つの例として、以下の操作を計算する必要がある場合を見てみましょう。

  1. 10%の税金を追加

  2. 割引の適用

  3. 価格を通貨形式にフォーマット

composeとpipeを使用しない場合

const tax = (amount) => amount + amount * 0.1;
const discount = (amount) => amount - 10;
const formatCurrency = (amount) => `$${amount.toFixed(2)}`;

const finalPrice = formatCurrency(discount(tax(100)));
console.log(finalPrice);

composeを使用した場合

const compose = (...functions) => (value) =>
  functions.reduceRight((acc, fn) => fn(acc), value);

const calculateTotalWithCompose = compose(formatCurrency, discount, tax);
console.log(calculateTotalWithCompose(100));

pipeを使用した場合

const pipe = (...functions) => (value) =>
  functions.reduce((acc, fn) => fn(acc), value);

const calculateTotalWithPipe = pipe(tax, discount, formatCurrency);
console.log(calculateTotalWithPipe(100));

上記の例から、composeとpipeを使用することで、コードの複雑さを軽減し、可読性を向上させることができることが分かります。

composeとpipeの選択について

どちらを選択するかは、関数の実行順序をどのように考えるかによって決まります。目的の結果を得るために、必ずしもcomposeとpipeを切り替える必要はありません。関数に渡す引数の順序を変更するだけで、どちらの方法でも同じ結果を得ることができます。
composeとpipeは、関数型プログラミングにおける強力なツールであり、複雑な機能を実現しながら、簡潔で再利用可能なコードを書くことを可能にします。次回、複雑な関数を書く必要が出てきた際は、ここで学んだ概念の適用を検討してみてください。それでは、楽しいコーディングを!


この記事は、2025 年 1 月に弊社のエンジニア Surendra Sedai が執筆し、日本語に翻訳したものです。
英語版はこちらをクリックしてください。
https://articles.wesionary.team/compose-and-pipe-in-javascript-a19102d36394


採用情報

私たちはプロダクト共創の仕組み化に取り組んでいます。プロダクト共創をリードするプロダクト・マネージャー、そして、私たちのビジョンを市場に届ける営業メンバーを募集しています!


開発パートナーをお探しの企業様へ

弊社は、グローバル開発のメリットを活かし、高い費用対効果と品質を両立しています。経験豊富で多様性のあるチームが、課題を正しく理解し、最適なシステムと優れた体験を実現します。業務システムの開発、新規事業の開発、業務効率化やDX化に関するお困りごと、ぜひ弊社にご相談ください。