JavaScriptにおける関数型プログラミングの実践ガイド

関数型パラダイムとは

関数型プログラミング(FP)は「値の変換」を中心に据えた設計思想である。OOPが「物」をモデル化するのに対し、FPは「関係=写像」をコード化する。React/Vue3 が再注目させた背景には、副作用を排除しテスタビリティとtree-shaking効率を高める点にある。

// 命令型
let price = 1000, tax = 1.1;
let total = price * tax;

// 関数型
const withTax = rate => price => price * rate;
const total = withTax(1.1)(1000);

ファーストクラス関数の威力

JavaScript の関数は単なる値。変数・配列・オブジェクトのプロパティに格納でき、引数や返り値としても扱える。

// 関数を値として保持
const greet = () => 'Hello FP';

// コントローラーの簡潔化
const Ctrl = {
  list: DB.list,
  get:  DB.get,
  add:  DB.add
};

高階関数で重複を削減

他の関数を受け取る/返す関数を高階関数と呼ぶ。ループや条件分岐を抽象化し、宣言的に記述できる。

// 独自 forEach
const each = (arr, fn) => {
  for (const v of arr) fn(v);
};

// 独自 filter
const select = (arr, pred) => {
  const res = [];
  for (const v of arr) if (pred(v)) res.push(v);
  return res;
};

each([1,2,3,4], console.log);
console.log(select([1,2,3,4], n => n % 2 === 0));

クロージャで状態を閉じ込める

内部関数が外部スコープの変数を参照し続ける仕組み。設定値を固定した再利用可能な関数を生成するのに便利。

// べき乗ジェネレータ
const power = exp => base => base ** exp;
const square = power(2);
const cube   = power(3);

console.log(square(5)); // 25
console.log(cube(3));   // 27

// 給与計算
const salary = base => bonus => base + bonus;
const senior = salary(800000);
console.log(senior(200000)); // 1000000

純粋関数で予測可能性を高める

同じ入力なら必ず同じ出力を返し、外部状態を変更しない関数を「純粋」と呼ぶ。キャッシュや並列実行が容易になる。

// 不純
let ratio = 1.08;
const addTax = price => price * ratio;

// 純粋
const addTax = rate => price => price * rate;
const addTax8 = addTax(1.08);

Currying で部分適用を実現

複数引数関数を、1引数ずつ受け取る関数列に変換する技法。設定値を固定し、再利用しやすい小粒関数を生み出す。

// 手動 curry
const checkAge = min => age => age >= min;
const adult = checkAge(20);
console.log(adult(25)); // true

// lodash curry
const _ = require('lodash');
const sum3 = _.curry((a,b,c) => a + b + c);
console.log(sum3(1)(2)(3)); // 6
console.log(sum3(1,2)(3));  // 6

関数合成でパイプラインを組む

小さな純粋関数を右から左へ連結し、一連の変換を1つの関数として表現する。lodash/fp の flowRight が活用できる。

const { flowRight: compose, map, join, split, toLower } = require('lodash/fp');

const slugify = compose(
  join('-'),
  map(toLower),
  split(' ')
);

console.log(slugify('Hello World')); // hello-world

デバッグトレースを差し込む

合成途中の値を覗き見るデバッグ関数を curry で作り、任意の位置に挿入できる。

const trace = label => val => {
  console.log(label, val);
  return val;
};

const debugSlug = compose(
  join('-'),
  trace('after lower'),
  map(toLower),
  trace('after split'),
  split(' ')
);

Functor で副作用を箱ごと封じる

値と変換ルールをカプセル化した「容器」を Functor と呼ぶ。map で内部値を安全に変換し、新しい容器を返す。

class Box {
  static of(x) { return new Box(x); }
  constructor(x) { this.$value = x; }
  map(f) { return Box.of(f(this.$value)); }
}

Box.of(5)
   .map(x => x * 2)
   .map(x => x + 1); // Box(11)

Maybe で null を扱う

null/undefined を map するとエラーになる問題を、Maybe 容器で回避する。

class Maybe {
  static of(x) { return new Maybe(x); }
  constructor(x) { this.$value = x; }
  isNothing() { return this.$value == null; }
  map(f) {
    return this.isNothing() ? Maybe.of(null) : Maybe.of(f(this.$value));
  }
}

Maybe.of('FP')
   .map(s => s.toUpperCase()); // Maybe('FP')

Maybe.of(null)
   .map(s => s.toUpperCase()); // Maybe(null)

Either でエラーハンドリング

失敗(Left)/成功(Right)の2つの分岐を表現し、例外を投げずにエラーを伝播させる。

class Left {
  constructor(x) { this.$value = x; }
  map() { return this; } // 値を変えない
}
class Right {
  constructor(x) { this.$value = x; }
  map(f) { return Right.of(f(this.$value)); }
  static of(x) { return new Right(x); }
}

const parseJson = str => {
  try {
    return Right.of(JSON.parse(str));
  } catch (e) {
    return new Left({ error: e.message });
  }
};

parseJson('{"a":1}').map(x => x.a); // Right(1)
parseJson('invalid').map(x => x.a); // Left({error:...})

IO Monad で非純粋を遅延実行

副作用を関数として包み、実行を呼び出し側に委ねることで「純粋な」コードを保つ。flatMap(または chain)でネストを平坦化。

const fs = require('fs');
const { IO } = require('ramda-fantasy');

const readFile = filename =>
  IO(() => fs.readFileSync(filename, 'utf8'));

const print = x =>
  IO(() => { console.log(x); return x; });

const program = readFile('package.json')
                .map(JSON.parse)
                .map(pkg => pkg.name)
                .chain(print); // 実行はここでは行われない

program.unsafePerformIO(); // ここで初めて実行

まとめ

関数型パラダイムは「値の変換パイプライン」を記述する手法である。ファーストクラス関数・高階関数・クロージャ・純粋関数・Currying・合成・Functor/Monad などの概念を組み合わせることで、副作用を最小限に抑え、テスタブルで再利用可能なコードを書くことができる。

タグ: javascript functional-programming lodash currying monad

6月17日 20:36 投稿