はじめに
Carbon というWebサービスをご存じでしょうか?
Carbon: https://carbon.now.sh/
ソースコードをシンタックスハイライト付きで画像化してくれる、ツイッター投稿などに便利なWebサービスです。この Carbon なのですが、初期画面には短い javascript のコードが表示されます。
const pluckDeep = key => obj => key.split(".").reduce((accum, key) => accum[key], obj);
const compose = (...fns) => res => fns.reduce((accum, next) => next(accum), res);
const unfold = (f, seed) => {
const go = (f, seed, acc) => {
const res = f(seed);
return res ? go(f, res[1], acc.concat([res[0]])) : acc;
};
return go(f, seed, []);
};
このコードは一体何を表しているのでしょうか?
アロー関数が連続していたりと javascript では普段見慣れない構文が並んでいたので、順番にひもといてみると、関数型プログラミングというキーワードが浮かび上がってきました。
おことわり
当方勉強中につき、関数型プログラミング自体の細かい解説は避けます。
前準備
これらの関数をひもとくには、2つの知識が必要です。
- 関数を返す関数
- Array.reduce() の第2引数
関数を返す関数
javascript では関数もオブジェクトであるため、通常の値と同様に、引数で渡したり返り値で返したりできます。
このうち、関数を返す関数の動きを追ってみましょう。
const add = a => (b => a + b);
const add2 = add(2);
// add2 = b => 2 + b
const result = add2(3); // 2 + 3 = 5
add は引数 a を受け取ると関数 b => a + b を返す関数です。
add(2) は引数 2 を受け取ったので、b => 2 + b を返しました。
add2 は引数 b を受け取ると 2 + b を返す関数になります。
()とadd2を省略して、下のように書けます。
// in short
const add = a => b => a + b;
const result = add(2)(3); // 5
アロー関数が連なっている表記のナゾは解けましたね。
この書き方の利点などはさておき、仕組みは理解されたでしょうか?
補足: 「カリー化された関数」で調べると関数型プログラミングのディープな世界に足を突っ込めます
Array.reduce() の第2引数
Array.reduce() は、第1引数に関数 (reducer) を渡すことで、配列からひとつの値を作り出す関数です。
reducer の第1引数にはひとつ前の reducer の出力が、第2引数には配列の値が順々に渡されます。
// accum - accumulate: 蓄積
const result = [2, 3, 4].reduce((accum, next) => accum + next); // (2 + 3) + 4
この Array.reduce() は第2引数として、reducer の初期値を与えることができます。
Array.prototype.reduce() - JavaScript | MDN
const result = [2, 3, 4].reduce((accum, next) => accum + next, 30);
// ((30 + 2) + 3) + 4
Array.reduce() は使いこなすと、map も filter もなんでもこなす、すごいやつなのですが、それはまた別の話。
// mapの代わり
const result = [3, 4, 5].reduce((accum, next) => [...accum, next + 1], []);
// [4, 5, 6]
関数をひもとく
それでは実際に関数をひもといてみましょう。
pluckDeep
const pluckDeep = key => obj => key.split(".").reduce((accum, key) => accum[key], obj);
// .reduce の第2引数: obj
// .reduce の返り値: obj[key][key][key]...
実際の使用例を見て流れを追ってみましょう。
// How to use
const pluckFooBar = pluckDeep('foo.bar');
const result = pluckFooBar({ foo: { bar: 'result!' } });
// key = 'foo.bar'
// key.split(".") = ['foo', 'bar']
// pluckFooBar = obj => (obj[foo])[bar]
// obj = { foo: { bar: 'result!' } }
// result = 'result!'
オブジェクトのプロパティにアクセスする関数だとわかります。
compose
const compose = (...fns) => res => fns.reduce((accum, next) => next(accum), res);
// .reduce の第2引数: res
// .reduce の返り値: next(next(next(res)))...
関数を合成する関数だとわかります。
// How to use
// Sample from https://ramdajs.com/docs/#compose
const calc = compose(Math.abs, a => a + 1, b => b * 2);
const result = calc(-4); // 10
// fns = [Math.abs, a => a + 1, b => b * 2]
// calc = res => ((Math.abs(res)) + 1) * 2
// res = -4
// result = ((Math.abs(-4)) + 1) * 2
上が使用例です。複数の関数を合成して、後から値を与えているのがわかりますね。
unfold
const unfold = (f, seed) => {
const go = (f, seed, acc) => {
const res = f(seed);
return res ? go(f, res[1], acc.concat([res[0]])) : acc;
};
return go(f, seed, []);
};
// f(seed) の返り値: [value, nextSeed] or false
// go() の返り値: [value, value, value, ...]
簡単な再帰関数だとはわかります。
しかしこのままでは用途が分からないですね。使用例を出してみましょう。
// How to use
// Sample from https://ramdajs.com/docs/#unfold
const f = n => n > 50 ? false : [-n, n + 10];
const seed = 10;
const result = unfold(f, seed);
// [-10, -20, -30, -40, -50]
シード値と関数からリストを生成する関数だとわかります。
最後に
カリー化, reduce から始まって、pluck, compose, unfold と、実はどれも関数型プログラミングの道具たち。ES6になってアロー関数が導入されたことで、とてもシンプルに書けるようになりました。初期画面のコードは、ES6で関数型プログラミングを始めませんか?という開発者からのメッセージなのでしょう。