はじめに
filterのように、Lodashは関数を引数とする関数を提供しています。
しかし、関数を指定すべき引数に対して、文字列や配列、オブジェクトを指定できるケースもあります。
そして、サンプルコードを読めば、それが省略記法であることは 何となく 分かります。
本記事では、この 何となく を しっかり にすることを目指します。
環境情報
Lodash 4.17.10
省略記法の例
以下はLodashのドキュメントのfilter関数のExampleです。
var users = [
{ 'user': 'barney', 'age': 36, 'active': true },
{ 'user': 'fred', 'age': 40, 'active': false }
];
_.filter(users, function(o) { return !o.active; });
// => objects for ['fred']
// The `_.matches` iteratee shorthand.
_.filter(users, { 'age': 36, 'active': true });
// => objects for ['barney']
// The `_.matchesProperty` iteratee shorthand.
_.filter(users, ['active', false]);
// => objects for ['fred']
// The `_.property` iteratee shorthand.
_.filter(users, 'active');
// => objects for ['barney']
filterの引数は以下の二つです。
- collection: フィルタリング対象の配列またはオブジェクト
- predicate: フィルタリングの条件を表す関数
最初の例は引数の定義に忠実にしたがった呼び出しとなっています。
一方で、2つめ以降の例は predicate
に関数ではなく、オブジェクトや配列が指定されています。
型の違いにびっくりしますが、Exampleのコメントを読めばそれが省略記法であることが分かります。
filterに限らず、他の関数(e.g., countBy, find)もこのような省略記法が使用できます。
さて、この省略記法って何者でしょうか?
- 関数を引数とする関数であれば、任意の関数で使用できるのでしょうか?
- Exampleで書かれている以外の省略記法ってないのでしょうか?
ドキュメントを読んでもExampleに書かれている以上の情報は見つかりませんでした。
Lodash 4 Cookbookの解説
何気なく検索してみると、Lodash4 Cookbook に良さげな説明が書かれていました。
以下、 Lodash 4 - Cookbook Predicates より引用です。
For lodash functions which accept predicates, e.g. _.find and _.filter, predicates can be specified using functions, strings, and objects.
If a function is provided, it’s used directly. The predicate matches if the function returns a truthy value.
If only a string is provided, it’s used to create a function using _.property as the predicate.
If an array that contains a string and a value is provided, the string and the value are used to create a function using _matchesProperty as the predicate.
If an object is provided, it’s used to create a function using _.matches as the predicate.
つまり、
- Exampleに書かれていた省略記法は、filterやfindに限らず、他のLodashの関数でも使用できる
- Exampleに書かれていたもの以外で省略記法は存在しない
と言えそうです。
Lodashのソースコード
Lodash4 Cookbookのおかげでルールは見えてきました。
せっかくなので、ソースコードの方も覗いてみましょう。
filter
以下はLodashのfilter関数の定義です。
function filter(collection, predicate) {
var func = isArray(collection) ? arrayFilter : baseFilter;
return func(collection, getIteratee(predicate, 3));
}
引数はドキュメントに書かれている通り、 collection
と predicate
です。
最初に、 collection
が配列かどうかに応じて用いるフィルターを選択しています。これについては深入りしないでおきます。
ポイントとなるのは、 return文の中にある getIteratee(predicate, 3)
です。
getIteratee
getIterateeの定義を覗いてみます。
function getIteratee() {
var result = lodash.iteratee || iteratee;
result = result === iteratee ? baseIteratee : result;
return arguments.length ? result(arguments[0], arguments[1]) : result;
}
読み解き難いですが、 lodash.js
で lodash.iteratee = iteratee;
と宣言されているので、基本的には result
はbaseIterateeになると思われます。
getIterateeには predicate
と 3
が引数として渡されるので、 arguments.length
は2です。
したがって、 baseIteratee(predicate, 3)
が評価されるはずです。
baseIteratee
baseIterateeの定義を覗いてみます。
function baseIteratee(value) {
// Don't store the `typeof` result in a variable to avoid a JIT bug in Safari 9.
// See https://bugs.webkit.org/show_bug.cgi?id=156034 for more details.
if (typeof value == 'function') {
return value;
}
if (value == null) {
return identity;
}
if (typeof value == 'object') {
return isArray(value)
? baseMatchesProperty(value[0], value[1])
: baseMatches(value);
}
return property(value);
}
Lodash4 Cookbookに書かれていたルールに対応していそうな内容です!
引数の value
に predicate
が渡されることに注意します。
まず、 value
が関数であるかをチェックし、関数ならばそのまま value
を返しています。
これは最初のExampleにおけるルートですね。
value
が関数でない場合、 value
が null
であるかをチェックしています。
null
ならばデフォルト値であるidentity関数を返しています。
これは、Exampleにはありませんでしたが、実はLodashのドキュメントに、 predicate
のデフォルト値が _.identity
であると書かれています。
value
が関数でも null
でもない場合、 value
の型が object
であるかをチェックしています。
ここで、 value
が配列であっても typeof value
は object
であることに注意してください。
つまり、 predicate
に配列またはオブジェクトが指定された場合のパターンであり、2つめと3つめのExampleにおけるルートです。
このケースでは、三項演算子を用いて以下の値を返しています。
-
value
が配列の場合:baseMatchesProperty(value[0], value[1])
- そうでない場合:
baseMatches(value)
baseMatchesProperty
と baseMatches
はそれぞれ、 _.matchesProperty
と _.matches
を実装している関数です。深追いはしませんが、それぞれ _.matchesProperty
と _.matches
で読み換えてよいかと思います。
まさにLodash4 Cookbookに書かれていたルールの通りです。
最後に、 value
が関数でも null
でも object
でもない場合に、 property(value)
を返しています。
これは value
が文字列のパターンであり、4つめのExampleにおけるルートですね。
期待通り、 _.property
を適用した結果を返しています。
長かったですが、以上より、Lodash4 Cookbookに書かれていたルールを確認できました!
Lodashのソースコードのまとめ
- Lodashの関数を引数にとる関数は、その引数に対して、getIteratee, baseIterateeを適用している
- getIteratee, baseIterateeは、以下のマッピングを行う
- 引数
predicate
が配列の場合、関数_matchesProperty(predicate[0], predicate[1])
にマッピングする - 引数
predicate
がオブジェクトの場合、 関数_matchesProperty(predicate)
にマッピングする - 引数
predicate
が文字列の場合、 関数_.property(predicate)
にマッピングする
- 引数
上記の仕組みにより、関数を引数として渡す代わりに、配列やオブジェクト、文字列を渡すことができる。また、その仕組みの核となるgetIteratee, baseIterateeはfilterのみに限らず、他の関数を引数にとる関数でも使い回されている。
おわりに
普段、何気なく使っているライブラリもソースコードを見てみると面白いものです。
Lodashの省略記法については、公式のドキュメントでもう少し詳しく書いたほうがよいのではないかと思いました。