業務でJavascriptを使っていますが、業務では普段使わないので、理解を深めるためにメモです。
高階関数とは
関数を引数や戻り値とする関数のことを言います。
例えば、下記のような関数が高階関数です。
コード例
function curry(fn, arity) {
return function curried() {
if (arity == null) {
arity = fn.length;
}
var args = [].slice.call(arguments);
if (args.length >= arity) {
return fn.apply(this, args);
} else {
return function() {
return curried.apply(this, args.concat([].slice.call(arguments)));
};
}
};
}
転載元:just-curry-it
コード例の関数curryはカリー化をする関数です。
カリー化とは、
複数の引数をとる関数を、引数が「もとの関数の最初の引数」で戻り値が「もとの関数の残りの引数を取り結果を返す関数」であるような関数にすること(あるいはその関数のこと)のことを言います。
引用元:wikipedia
です。
例えば、下記のようなaddという関数でカリー化した場合は下記のようになります。
function add(a, b, c) {
return a + b + c;
}
const val1 = add(1,2,3); // 6
const val2 = curry(add)(1)(2)(3); // 6, add(1,2,3)と同じ結果になる
本記事はこのcurry関数の解説をして、高階関数の理解をします。
何が嬉しいか
引数を途中まで反映した関数を作り出すことができます。
これにより、処理を分けて書くことが容易になり、分けて書くことで再利用性が向上します。抽象化もしやすいです。
例えば、コード例では下記のように使うことができます。
function add(a, b, c) {
return a + b + c;
}
const add_1 = curry(add)(1); // add関数に1だけ適用した状態
const add_1_2 = add_1(2); // add関数に1を適用した後に、2を適用した状態
const val1 = add_1_2(3); // 6
const val2 = curry(add)(1)(2)(3); // 6, add_1_2(3)と同じ結果
const add_1_4 = add_1(4); // add関数に1を適用した後に、4を適用した状態
コード例のcurry関数の解説
では、本題の高階関数を理解をします。とてもわかりやすいので、コード例のcurry関数を使います。
function curry(fn, arity) {
return function curried() {
if (arity == null) {
arity = fn.length;
}
var args = [].slice.call(arguments);
if (args.length >= arity) {
return fn.apply(this, args);
} else {
return function() {
return curried.apply(this, args.concat([].slice.call(arguments)));
};
}
};
}
function add(a, b, c) {
return a + b + c;
}
const val = curry(add)(1)(2)(3);
上記のコードの時、curry(add)(1)(2)(3)を見ていきます。
まず、curry関数は引数がadd関数で呼び出され、戻り値であるcurried関数は引数が1で呼び出されます。
なので、fn=add、arity=undefined、arguments=[1](Array-like)で実行されます。
参考:Array-like
fn.lengthは、関数の引数の数になりますので、arity=3となります。
argsはArray-likeをArrayに変換してコピーしているだけで、args=[1]になります。
args.length >= arityはfalseになり、
function() { return curried.apply(this, args.concat([].slice.call(arguments))); }が戻り値になります。
この時のthisはcurriedで、この戻り値の関数は次の引数2で呼び出されるため、argumentsは[2](Array-like)になります。そのため、args.concat([].slice.call(arguments))は[1,2]になります。
function curry(fn, arity) { // addとundefinedが引数
return function curried() { // [1](Array-like)が引数
if (arity == null) {
arity = fn.length; // 3
}
var args = [].slice.call(arguments); // [1]をコピー
if (args.length >= arity) { // falseなのでこっちに入らず
return fn.apply(this, args);
} else { // こっちに入る
return function() {
return curried.apply(this, args.concat([].slice.call(arguments))); // argumentsは[2]で、[1,2]が引数で、curriedが呼び出される
};
}
};
}
引数[1,2]で呼び出されたcurried関数は、args=[1,2]のため、args.length >= arityがfalseになり、再度function() { return curried.apply(this, args.concat([].slice.call(arguments))); }が戻り値になります。
この戻り値の関数は次の引数3で呼び出されるため、argumentsは[3](Array-like)になります。そのため、args.concat([].slice.call(arguments))は[1,2,3]になります。
function curried() { // [1,2](Array-like)が引数
if (arity == null) { // arity=3なのでここは入らず
arity = fn.length;
}
var args = [].slice.call(arguments); // [1,2]をコピー
if (args.length >= arity) { // falseなのでこっちに入らず
return fn.apply(this, args);
} else { // こっちに入る
return function() {
return curried.apply(this, args.concat([].slice.call(arguments))); // argumentsは[3]で、[1,2,3]が引数で、curriedが呼び出される
};
}
};
引数[1,2,3]で呼び出されたcurried関数は、args=[1,2,3]のため、args.length >= arityがtrueになります。
このため、fn.apply(this, args)が戻り値になります。
fn=add、this=fn、args=[1,2,3]となり、addが引数1,2,3で呼び出されることになります。
function curried() { // [1,2,3](Array-like)が引数
if (arity == null) { // arity=3なのでここは入らず
arity = fn.length;
}
var args = [].slice.call(arguments); // [1,2,3]をコピー
if (args.length >= arity) { // 3>=3で、trueなのでこっちに入る
return fn.apply(this, args); // fn=add
} else {
return function() {
return curried.apply(this, args.concat([].slice.call(arguments)));
};
}
};
終わり
余力があれば、もっと抽象化された使い方がされているコードも忘れてしまうので、メモ代わりにQiitaに残したい