Carbonの初期画面のソースコードをひもとく


はじめに

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つの知識が必要です。

  1. 関数を返す関数
  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で関数型プログラミングを始めませんか?という開発者からのメッセージなのでしょう。