「カリー化」を例でさっくり理解する

カリー化を例でさっくり理解する

「カリー化」です。

いきなり定義から入ると数学っぽくなってやる気がなくなるので、例から入ります。以下、JavaScript です。

カリー化の基本

べき乗を求める関数 pow(x, y) があるとします。

function pow(x, y) {
    return Math.pow(x, y);
}

console.log(pow(2, 3)); // -> 8
console.log(pow(2, 4)); // -> 16

次に、底 x が先に与えられていて、「x の y 乗を求める関数」が欲しいとします。つまり、「引数に x を入れたら、「 x の y 乗を求める関数」を返してくれるような関数」を作りたい。

JavaScript は関数を他のオブジェクトと同じように返り値にできるので、そういう関数が作れます。

function powBaseBy(x) {
    return function(y) {
        return Math.pow(x, y);
    };
}

関数 powBaseBy(x) は、関数 pow(x, y) の第一引数だけを先に決めておく感じです。挙動は次のとおり。

var pow2 = powBaseBy(2); // 2 のべき乗を求める関数

console.log(pow2(1)); // -> 2
console.log(pow2(2)); // -> 4
console.log(pow2(3)); // -> 8

var pow3 = powBaseBy(3); // 3 のべき乗を求める関数

console.log(pow3(1)); // -> 3
console.log(pow3(2)); // -> 9
console.log(pow3(3)); // -> 27

関数 pow(x, y) から関数 powBaseBy(x) を作ることが、カリー化です。

一般に、カリー化とは、複数の引数を持つ関数f(x, y)から、「第一引数xだけを決めておいて残りが引数が入力待ちになる関数を生成する関数F(x)」を作ることです。言葉で説明するとややこしいですね。

要するに、カリー化された関数は、まず「関数を返り値とする関数」です。そして、値を渡してやることで、具体的な関数を作ってくれます。関数に変数をちょっとずつ渡して、ちょっとずつ違う関数を作りたいときに役立ちます。

抽象化して再利用

次の例です。

生徒のテストの点数をオブジェクトで管理しているとします。

var students = [
    {
        math: 80,
        english: 90
    },
    {
        math: 60,
        english: 99
    },
    {
        math: 100,
        english: 40
    }
]

数学と英語それぞれの合計点を計算しましょう。

var mathSum = students.reduce((sum, student) => sum + student.math, 0);
var engSum  = students.reduce((sum, student) => sum + student.english, 0);

console.log(mathSum); // -> 240
console.log(engSum);  // -> 229

mathSumengSum の計算はよく似ているので、抽象化してまとめられそうですね。 そこで、次のような感じでカリー化します。

function calcSumOf(propName) {
    return function(array) {
        return array.reduce((sum, object) => sum + object[propName]);
    };
}

var calcMathSum = calcSumOf('math');
var calcEngSum  = calcSumOf('english');

var mathSum = calcMathSum(students);
var engSum  = calcEngSum(students);

console.log(mathSum); // -> 240
console.log(engSum);  // -> 229

これで、同じ処理を二度書かなくてよくなりました。

「一番良い感じ」のものを選ぶ関数

生徒たちの中から「いちばん良い生徒」を決めたい。ところが、「良い」の基準はまだ決まっていないとします。「数学と英語の合計点」で評価するとか、「数学と英語のうち大きい方の点数」で評価するとか、いろいろな評価方法が考えられます。

ここで作りたいのは、オブジェクトの配列から「一番いい感じのオブジェクト」を選ぶ関数を関数です。どんなオブジェクトが「良い」のかはまだ決まっていない、つまり「良い」を定める評価関数はあとで引数として与えることになります。

カリー化された関数はこんな感じ。

function best(evalFunc) {
    return function(objects) {
        var evaluated = objects.map(obj => evalFunc(obj));
        var max = Math.max.apply(null, evaluated); // 配列の最大値を求める
        var maxIndex = evaluated.indexOf(max);
        return objects[maxIndex];
    }
}

引数 evalFunc に生徒の評価方法を入れることで、「いちばん良い生徒」を決める関数が作られます。

// 「良い」=「合計点が高い」
var bestBySum = best(student => student.math + student.english);
var bestStudent1 = bestBySum(students);

console.log(bestStudent); // -> { math: 80, english: 90 }

// 「良い」=「数英の最高点が高い」
var bestByMax = best(student => student.math > student.english ? student.math : student.english);
var bestStudent2 = bestByMax(students);

console.log(bestStudent2); // -> { math: 60, english: 100 }

なんか楽しい

カリー化、なんか楽しいですね。

参考図書は、JavaScriptで学ぶ関数型プログラミング