Kodokon kodokon.com

関数型パターン: カリー化、合成、イミュータビリティ

カリー化、関数合成、そしてイミュータブルなデータでコードを構造化し、予測可能性とテスト容易性を手に入れます。

9 分 · 3 問

このレッスンを Kodokon で開く

JavaScript は関数を第一級の値(first-class value)として扱い、これが関数型パターンへの扉を開きます。カリー化(currying)は、n 個の引数を取る関数を、単一引数の関数の連鎖に変えます。f(a, b, c)f(a)(b)(c) になります。一度に一部の引数を固定する部分適用(partial application)fn.bind(null, a))と混同しないでください。カリー化は、汎用的な関数から特化した関数を作るときに真価を発揮します。

JAVASCRIPT
const curry = (fn) => {
  const arity = fn.length;
  return function curried(...args) {
    if (args.length >= arity) return fn(...args);
    return (...rest) => curried(...args, ...rest);
  };
};

const add3 = (a, b, c) => a + b + c;
const addCurried = curry(add3);

console.log(addCurried(1)(2)(3)); // 6
console.log(addCurried(1, 2)(3)); // 6
関数のアリティに基づく汎用的なカリー化。

仕様上の細かい点。fn.length は、最初のデフォルト引数またはレスト引数より前にある引数だけを数えます。(a, b = 1) => {} の length は 1、(...args) => {} は 0 です。したがってアリティに基づく curry には、単純な引数を持つ関数を渡さなければなりません。合成(composition)は次の一歩です。小さな純粋関数を、読みやすいパイプラインへと組み立てます。

JAVASCRIPT
const pipe = (...fns) => (input) =>
  fns.reduce((acc, fn) => fn(acc), input);

const trim = (s) => s.trim();
const lower = (s) => s.toLowerCase();
const dashes = (s) => s.replace(/\s+/g, "-");

const toSlug = pipe(trim, lower, dashes);
console.log(toSlug("  Hello World  "));
// "hello-world"
pipe は関数を左から右へ適用します。

pipe(f, g, h)(x)h(g(f(x))) を計算します。実行順に読めるのです。compose の変種は、数学の記法のように右から左へ適用します。これらのパイプラインが価値を持つのは、関数が純粋(pure)である場合、つまり副作用も隠れた依存もない場合だけです。それにはイミュータブル(immutable)なデータが求められます。オブジェクトを変更するのではなく、その新しいバージョンを生成するのです。すると参照の等価性だけで変更を検出できるようになります(React の再調整の核心)。

JAVASCRIPT
const state = Object.freeze({
  user: { name: "Ada" },
  tags: ["js"]
});

const next = {
  ...state,
  tags: [...state.tags, "fp"]
};

const deepCopy = structuredClone(state);
deepCopy.user.name = "Grace"; // independent copy
変更ではなくコピーによる新しいバージョン。

理解度チェック

このレッスンの要点をしっかり覚えているか確認しましょう。

  1. カリー化と部分適用の違いは何ですか?
    • 違いはない、同義語である
    • カリー化は f(a, b, c) を f(a)(b)(c) に変える。部分適用は一度の呼び出しで一部の引数を固定する
    • 部分適用は bind でしか、カリー化はアロー関数でしか動作しない
    • カリー化は 2 引数の関数にしか適用できない
  2. pipe(f, g, h)(x) は何と等価ですか?
    • f(g(h(x)))
    • h(g(f(x)))
    • g(f(h(x)))
    • f(x) + g(x) + h(x)
  3. const s = Object.freeze({ a: { b: 1 } }) のあと、代入 s.a.b = 2 は機能しますか?
    • いいえ、freeze はオブジェクトツリー全体を凍結する
    • いいえ、strict モードでなくても TypeError が投げられる
    • はい、freeze は浅いから。ネストされたオブジェクト s.a は凍結されない
    • はい、ただし非 strict モードでのみ