「30 seconds of code」個人的10選/30秒で楽しめるES2015パターン集
前書き
ES2015はJavaScriptにとって大きな変革で、開発者にとっては大きな困惑を生んでいるようです。
いろんなAPIが増えたり、新しい構文が出たりでなかなか使いこなすのは確かに大変だと思います。
そこでES2015を効率よく習得できるパターン集が出されています。1問30秒で解けるくらいの問題が、大量に並んでいて非常に勉強になります。
それだけでは寂しいので、その中から個人的に気に入っている問題をとりあげて簡単に解説します。
30秒考えて、何をやっているのかわからなかった人はぜひ上のサイトに挑戦してみましょう。
問題10選
pipeFunctions
const pipeFunctions = (...fns) => fns.reduce((f, g) => (...args) => g(f(...args))); // example const add5 = x => x + 5; const multiply = (x, y) => x * y; const multiplyAndAdd5 = pipeFunctions(multiply, add5); multiplyAndAdd5(5, 2); // 15
これはいわゆる関数合成のための関数ですね。関数を順番に適用していって、最終結果を出力しています。
Arrayのreduceメソッドは配列の要素一つずつに順に関数を適用していくという処理なので、関数の配列を作って回すとこういうことができるという好例です。
また、...(ピリオド3つ)という記号はスプレッド演算子といって、配列を動的に展開するものです。
これを使うことで引数を可変長で受け取れたり、新しい配列を簡単に作れたら非常に便利なことができます。ここでは、配列を引数に受け取らない関数で使えるように、使っています。
deepFlatten
const deepFlatten = arr => [].concat(...arr.map(v => (Array.isArray(v) ? deepFlatten(v) : v))); // examples deepFlatten([1, [2], [[3], 4], 5]); // [1,2,3,4,5]
多次元の配列を全部つぶして、1次元の配列に戻している関数です。
一番に目につくのはmapの中で再帰処理が書かれていることですね。何階層あるかわからないので、再帰で書くしかないわけですが停止条件をきちんとかければ、1行の再帰でこういった表現ができるのは注目すべき点です。
再帰自体表現力が高いですが、ES2015のAPIを使えばさらに鋭い書き方ができるわけですね。
また、配列の結合をconcatでやっているところも注目です。concatはpushと違い元の配列を壊さないのでこういった関数的な処理ではよく使います。
groupBy
const groupBy = (arr, func) => arr.map(typeof func === 'function' ? func : val => val[func]).reduce((acc, val, i) => { acc[val] = (acc[val] || []).concat(arr[i]); return acc; }, {}); // examples groupBy([6.1, 4.2, 6.3], Math.floor); // {4: [4.2], 6: [6.1, 6.3]} groupBy(['one', 'two', 'three'], 'length'); // {3: ['one', 'two'], 5: ['three']}
そろそろ見慣れた構文ばかりで、すらすら読めるようになってきたのではないでしょうか。
今回はreduceで条件に従ってグループ化したオブジェクトを作っています。reduceは最大値や平均、合計を出すために使われることが多いですが、リストからオブジェクトへの集約もよくあるパターンです。
あとは3項演算子が出てきていますが、ES2015からはラムダ式で一文で書ききりたいことが多いので、よく使います。{}(ブレース)はできるかぎり、使わないほうがきれいに書けることが多いですね。
mapObject
const mapObject = (arr, fn) => (a => ( (a = [arr, arr.map(fn)]), a[0].reduce((acc, val, ind) => ((acc[val] = a[1][ind]), acc), {}) ))(); // examples const squareIt = arr => mapObject(arr, a => a * a); squareIt([1, 2, 3]); // { 1: 1, 2: 4, 3: 9 }
本質的にはgroupByに近いことをしています。そろそろreduceの力に気づいてきたのではないでしょうか。
reduceは引数としてindexを受け取れます。それを使って、オブジェクトのキーを設定しているところとかがすごく工夫を凝らしてあって面白いですね。
あとは、代入文のあとに,(カンマ)を使って、返り値をaccに戻しているところとかの技巧を感じとれたらなかなかES2015が身についていると思います。
sampleSize
const sampleSize = ([...arr], n = 1) => { let m = arr.length; while (m) { const i = Math.floor(Math.random() * m--); [arr[m], arr[i]] = [arr[i], arr[m]]; } return arr.slice(0, n); }; // examples sampleSize([1, 2, 3], 2); // [3,1] sampleSize([1, 2, 3], 4); // [2,3,1]
リストの中から、重複していない要素を指定された数だけ取り出す関数です。ゲームとかではよく使いますね。ビンゴゲームに近いものなら、これだけでだいぶ組めそうです。
手続き型に近く、あんまりES2015さを感じない例ではありますが、よく見ると [arr[m], arr[i]] = [arr[i], arr[m]];という見慣れない文があります。
これは分配束縛(destructing)と呼ばれるもので、ここでは要素の入れ替えを行っています。
関数型言語では有名な機能で、リストから最初の要素や残りの要素を取り出したり、必要な数だけ配列の要素を変数に取り出したり出来ます。ES2015の中でも、かなり大きな変更の一つといえるでしょう。
ぜひ使いこなしましょう。
chainAsync
const chainAsync = fns => { let curr = 0; const next = () => fns[curr++](next); next(); }; // examples chainAsync([ next => { console.log('0 seconds'); setTimeout(next, 1000); }, next => { console.log('1 second'); } ]);
非同期で処理をつなげていく関数です。
PromiseやらRxJSやらがある時代で別に必要ではないのですが、それらの仕組みをわかりやすくエミュレートしているので個人的には好みです。
next関数が自分自身を再帰的に渡しているのがとても格好いいですね。クロージャーを使って、indexと関数の配列をつかんでいて、それを停止条件にしている感じですかね。それによって、短いながらも多くのことをしています。
curry
const curry = (fn, arity = fn.length, ...args) => arity <= args.length ? fn(...args) : curry.bind(null, fn, arity, ...args); // examples curry(Math.pow)(2)(10); // 1024 curry(Math.min, 3)(10)(50)(2); // 2
関数の引数を減らして、残りの引数を受け取る関数を返す関数です。開発者であれば、カリー化という言葉を聞いたことがあるでしょう。
やっていることはクロージャを設定して、引数を1つ消費しているだけです。こうした処理は昔から書くことができましたが、スプレッド演算子とアロー演算子のおかげでだいぶ短く書くことができるようになりましたね。
digitize
const digitize = n => [...`${n}`].map(i => parseInt(i)); // examples digitize(123); // [1, 2, 3]
10進数の数値をばらして配列にしているだけなので、シンプルな関数です。
``(テンプレートリテラル)を使って、数値を文字にしています。そして、それをスプレッド演算子で展開したうえで、新しい配列を作っています。
なんでこんな無駄なことをしているのかといえば、文字列はそのままだとES2015のArrayのメソッドを使えないからです。なので、一度ばらしてから、配列に展開しているわけです。
短いコードのなかによくこんなに詰め込めたなと感心します。
sumPower
const sumPower = (end, power = 2, start = 1) => Array(end + 1 - start) .fill(0) .map((x, i) => (i + start) ** power) .reduce((a, b) => a + b, 0); // examples sumPower(10); // 385 sumPower(10, 3); //3025 sumPower(10, 3, 5); //2925
ここまで読んでもらえた方には特に新しいことはないです。Arrayを使うことでうまく、APIを使えるようにしているところとか、mapのindexをうまく使っているところあたりくらいでしょうか。
しかし、短い密度でいろんなことをしているので、情報量が多く、まとめにはいいかなぁと思い選びました。
capitalize
const capitalize = ([first, ...rest], lowerRest = false) => first.toUpperCase() + (lowerRest ? rest.join('').toLowerCase() : rest.join('')); // examples capitalize('fooBar'); // 'FooBar' capitalize('fooBar', true); // 'Foobar'
先頭を大文字にする関数です。よくあるパターンですね。
ここではfirstという変数を分配束縛で取り出しています。そして、残りをrestという変数に入れています。こういう先頭とそれ以外という処理は関数型では頻出です。
こういう分配束縛ができると、スマートに書けることを頭に入れておくと役に立ちます。
まとめ
皆さんどうでしたか。ES2015というと、クラス構文とラムダ式、あとはmap、filterあたりにしか注目してこなかったのではないでしょうか。
でも、見てきたようにスプレッド演算子、分配束縛、reduceも同じように高い表現力があるんだよということをぜひ伝えたいと思います。
JavaScriptはもはや表現力の高い言語で、短いコードに高度な情報を詰め込めます。変化が多くてつらいとか、文法が難しいとよく聞きますが、個人的にはこんなに書きやすい言語はそうないと思っています。
ぜひ、「30 seconds of code」で遊んでみて、JavaScriptの神髄を味わってみてはどうでしょうか。