Edited at

lodash/fp を使ってJavaScriptの配列操作をよりエレガントにする

More than 1 year has passed since last update.

この記事はリクルートライフスタイル Advent Calendar 2017の記事です

JSで配列やオブジェクトの操作をする際のUtilityライブラリとして広く使われているlodashですが、lodashにはlodash/fpというモジュールが用意されています。今回はそのlodash/fpを使って、JSでの配列操作をよりエレガントに書く方法を紹介します。


はじめに

この記事では、下記のような問いを共通の課題とします。

// このような配列あるとします

const users = [
{ id: 1, name: 'travis', age: 36, active: true },
{ id: 2, name: 'fred', age: 40, active: false },
{ id: 3, name: 'berney', age: 31, active: true },
{ id: 4, name: 'pebbles', age: 33, active: false }
]
// ユーザーのIDをアクティブactiveと非アクティブinactiveに分けて、若い順に取得してください
console.log(result)
// 期待するoutput: { active: [3, 1], inactive: [4, 2] }


Plain JavaScript

まずはプレーンなJSの例です

const sortedUsers = users.sort((a, b) => a.age > b.age)

const activeUsersId = sortedUsers.filter(({active}) => active).map(({id}) => id)
const inactiveUsersId = sortedUsers.filter(({active}) => !active).map(({id}) => id)

result = { active: activeUsersId, inactive: inactiveUsersId }

最近プレーンなJSでもmapやfilterなどの関数が使えるようになり、以前よりだいぶ配列操作がしやすくなりましたが、もう一歩という感じですね。


lodash

次にlodashの例です。

import { chain, sortBy, mapValues, value } from 'lodash'

const result = chain(users)
.sortBy('age')
.groupBy(({active}) => active ? 'active' : 'inactive')
.mapValues(group => group.map(user => user.id))
.value()

lodashであれば、各関数はよりシンプルに記述することができますし、chainを使ってストリーム的な処理をすることができます。

ただし、lodashの関数には破壊的なものもあり、また関数を共通化したい時にもうまく機能してくれません。


lodash/fp

そして、lodash/fpを使った場合です。

import flow from 'lodash/fp/flow'

import sortBy from 'lodash/fp/sortBy'
import groupBy from 'lodash/fp/groupBy'
import mapValues from 'lodash/fp/mapValues'

const result = flow(
sortBy(({age}) => age),
groupBy(({active}) => active ? 'active' : 'inactive'),
mapValues(group => group.map(user => user.id))
)(users)

...あれ、あまり変わらない?と思うかもしれません。

確かに上の例を見る限りあまり代わり映えなさそうに思えます。

では、下の例だとどうでしょうか。

import flow from 'lodash/fp/flow'

import sortBy from 'lodash/fp/sortBy'
import groupBy from 'lodash/fp/groupBy'
import mapValues from 'lodash/fp/mapValues'

const sortByAge = sortBy(({age}) => age)

const result = flow(
sortByAge(),
groupBy(({active}) => active ? 'active' : 'inactive'),
mapValues(group => group.map(user => user.d))
)(users)

若い順にソートする処理を切り出すことができました!

配列の処理がコード内に増えてきて、共通のロジックが出てきた時に切り出しができると、よりDRYで予測可能なコードにすることができます。

それでは、何故lodash/fpではこのようなテクニックが可能になるのかについて、lodashとの違いをベースに説明してみます。


Lodash と lodash.fpの違い

Lodashとlodash/fpの最大の違いは、引数の渡す順番です。

Lodashの提供する関数は適用するデータを最初の引数として受け取り、イテレータを後に書きます。

_.filter([1, 2, 3], x => x % 2 === 0)

一方で、lodash/fpの提供する関数は、イテレータを先に受け取り、適用するデータを最後の引数として受け取ります。

fp.filter(x => x % 2 === 0)([1, 2, 3])

また、lodash/fpでは、引数の数が満たされなかった場合、残りの引数を受けとる新たな関数を返します。

// 引数が満たされているので実行される

fp.filter(x => x % 2 === 0)([1, 2, 3])

// 引数がみたされていないので、残りの引数を受けとる新たな関数を返す
const evenFilter = fp.filter(x => x % 2 === 0)

// 残りの引数を受け取れば、実行される(使い回しがしやすい!)
evenFilter([1, 2, 3])
evenFilter([10, 20, 30])

このように、lodash/fpでは普通のlodashで提供されている関数を、イテレータとイテレートされるデータを逆にし(iteratee-first, data-last)、関数を返す関数にする(currying)ことで、ビジネスロジックを関数に閉じ込めて(残りの引数を待っている状態)、各所で使い回しができるようになっています。

このようなテクニックを、関数型プログラミングの世界では、関数合成(functional composition)と言ったりします。あえてこの記事では関数型という言葉をなるべく使わずに紹介してきましたが、lodash/fpのやり方が良さそうと思った方は、関数型プログラミングについて調べてみると面白いかもしれません。


参考リンク

lodash FP-Guide

why using chain is a mistake

Functional Programming With Lodash/FP