関数型パラダイムとは
関数型プログラミング(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 などの概念を組み合わせることで、副作用を最小限に抑え、テスタブルで再利用可能なコードを書くことができる。