Deno標準ライブラリ0.107.0で増強されたcollectionsの紹介
Deno標準ライブラリのバージョン0.107.0が公開されました。
こちらで増強されたcollectionsというモジュールを紹介します。
その名の通り、集合の扱いを支援してくれるモジュールです。
本記事では0.107.0のリリースで追加された関数を紹介します。
既存のものは以下の記事で解説していますので、あわせてご覧ください。
associatewith
配列と関数を引数に取り、配列の各値をキー、関数で変換された結果を値をとするレコードを返します。
キーが重複する場合はあとに来たもので上書きされます。
以前追加されたassociateByとはキー・バリューの関係が逆になります。
const fruits = [
"apple",
"banana",
"cherry",
"kiwi",
"coconut",
];
assertEquals(
associateWith(fruits, (it) => it.length),
{
"apple": 5,
"banana": 6,
"cherry": 6,
"kiwi": 4,
"coconut": 7,
},
);
配列の値をそのままキーにするため、引数の配列はstring[]
でなければなりません。
drop/take系
Array.prototype.filter()を途中で停止するような処理で、部分配列を作成する関数が複数追加されています。
dropWhile
配列と関数を引数に取り、述語関数が真になるまで(while)の要素を除去(drop)します。
逆に言うと、述語関数が最初に偽を返す要素以降の部分配列を抽出します。
const fruits = [
"apple",
"banana",
"cherry",
"kiwi",
"cherry",
"coconut",
];
assertEquals(
dropWhile(fruits, (it) => it !== "cherry"),
[
"cherry",
"kiwi",
"cherry",
"coconut",
],
);
dropLastWhile
上のdropLast
と同じ処理を、後ろから走査して行います。
const fruits = [
"apple",
"banana",
"cherry",
"kiwi",
"cherry",
"coconut",
];
assertEquals(
dropLastWhile(fruits, (it) => it !== "cherry"),
[
"apple",
"banana",
"cherry",
"kiwi",
"cherry",
],
);
takeWhile
配列と関数を引数に取り、述語関数が真になるまで(while)の要素を抽出(take)します。
前述のdropWhile
で捨てられる部分が返ります。
const fruits = [
"apple",
"banana",
"cherry",
"kiwi",
"cherry",
"coconut",
];
assertEquals(
dropWhile(fruits, (it) => it !== "cherry"),
[
"apple",
"banana",
],
);
同一の引数を取るdropWhile
の結果と足しあわせると、元の配列が得られます。
const fruits = [
"apple",
"banana",
"cherry",
"kiwi",
"cherry",
"coconut",
];
assertEquals(
[
...takeWhile(fruits, (it) => it !== "cherry"),
...dropWhile(fruits, (it) => it !== "cherry"),
],
fruits,
);
takeLastWhile
dropWhile
に対するtakeWhile
と同じく、dropLastWhile
に対するtakeLastWhile
も用意されています。
dropLastWhile
で捨てられる部分が返ります。
const fruits = [
"apple",
"banana",
"cherry",
"kiwi",
"cherry",
"coconut",
];
assertEquals(
takeLastWhile(fruits, (it) => it !== "cherry"),
[
"coconut",
],
);
dropLastWhile
の結果と足し合わせることで元の配列が得られます。
const fruits = [
"apple",
"banana",
"cherry",
"kiwi",
"cherry",
"coconut",
];
assertEquals(
[
...dropLastWhile(fruits, (it) => it !== "cherry"),
...takeLastWhile(fruits, (it) => it !== "cherry"),
],
fruits,
);
後ろから走査しているので、takeWhile
とdropWhile
を組み合わせた場合とは順序が逆になります。
findSingle
配列と関数を引数に取り、述語関数が真になる要素が一つしかない場合はその要素を返します。
複数ある場合、または一つもない場合はundefined
が返ります。
const fruits = [
{ name: "apple", count: 21 },
{ name: "banana", count: 20 },
{ name: "cherry", count: 84 },
{ name: "kiwi", count: 34 },
{ name: "coconut", count: 12 },
];
assertEquals(
findSingle(fruits, (it) => it.count > 40),
{ name: "cherry", count: 84 },
);
assertEquals(
findSingle(fruits, (it) => it.count > 20),
undefined,
);
assertEquals(
findSingle(fruits, (it) => it.count < 10),
undefined,
);
なお、述語関数はオプショナル引数です。省略すると初期値の(_) => true
が使用され、「配列が1要素かどうか」の判定になります。
assertEquals(
findSingle([]),
undefined,
);
assertEquals(
findSingle([1]),
1,
);
assertEquals(
findSingle([1, 2]),
undefined,
);
Enumerable#one?っぽいですね。
これは(なんとなく)使い途が多そうに思えました。
firstNotNullishOf
配列と関数を引数に取り、述語関数の返り値がnullish
にならない最初の結果を返します。
const fruits = [
{ name: "apple", count: null },
{ name: "banana", count: undefined },
{ name: "cherry", count: 84 },
{ name: "kiwi", count: 34 },
{ name: "coconut", count: 12 },
];
assertEquals(
firstNotNullishOf(fruits, (it) => it.count),
84,
);
// mapNotNullish
assertEquals(
mapNotNullish(fruits, (it) => it.count)[0],
84,
);
上記の通り、0.104.0の際に追加されたmapNotNullishの最初の要素のみを使用する場合と同じ動作になります。
includesValues
レコード判定系メソッドです。与えられた値を持つキーが存在すればtrue
を、なければfalse
を返します。
const fruits = {
apple: 21,
banana: 20,
cherry: 84,
kiwi: 34,
coconut: 12,
};
assertEquals(
includesValue(fruits, 34),
true,
);
コードを確認したところ、比較には===
が使われていました。
以下のように、===
で比較できないものはfalse
になります。
const fruits = {
apple: { count: 21 },
banana: { count: 20 },
cherry: { count: 84 },
kiwi: { count: 34 },
coconut: { count: 12 },
};
assertEquals(
includesValue(fruits, { count: 34 }),
false,
);
distinctとdistinctByみたいに、比較関数を調整するマイナーチェンジ版が出そうな予感…。
max/min系
配列から最大・最小を抽出する関数が複数追加されています。
maxBy/minBy
配列と関数を引数に取り、関数の返り値が最大・最小になる要素を返します。
わかりやすいですね。
const fruits = [
{ name: "apple", count: 21 },
{ name: "banana", count: 20 },
{ name: "cherry", count: 84 },
{ name: "kiwi", count: 34 },
{ name: "coconut", count: 12 },
];
assertEquals(
maxBy(fruits, (it) => it.count),
{ name: "cherry", count: 84 },
);
assertEquals(
minBy(fruits, (it) => it.count),
{ name: "coconut", count: 12 },
);
maxOf/minOf
上記のmaxBy
/minBy
と類似していますが、こちらは与えられた関数の返り値の最大値・最小値をそのまま返します。
const fruits = [
{ name: "apple", count: 21 },
{ name: "banana", count: 20 },
{ name: "cherry", count: 84 },
{ name: "kiwi", count: 34 },
{ name: "coconut", count: 12 },
];
assertEquals(
maxOf(fruits, (it) => it.count),
84,
);
assertEquals(
minOf(fruits, (it) => it.count),
12,
);
maxWith/minWith
配列と比較関数を引数とし、その比較関数で最大・最小を求めます。
比較関数はArray.prototype.sort()に渡すものと同じく、比較結果を数値の正負として返す必要があります。
const fruits = [
{ name: "apple", count: 21 },
{ name: "banana", count: 20 },
{ name: "cherry", count: 84 },
{ name: "kiwi", count: 34 },
{ name: "coconut", count: 12 },
];
assertEquals(
maxWith(fruits, (a, b) => a.count - b.count),
{ name: "cherry", count: 84 },
);
assertEquals(
minWith(fruits, (a, b) => a.count - b.count),
{ name: "coconut", count: 12 },
);
与えられた比較関数を使ってsort()
した結果の最初・最後の要素を返す、と考えれば良さそうです。
reduceGroups
配列を値とするレコードと関数を引数とし、レコードの各値を与えられた関数を使ってArray.prototype.reduce()します。
const fruits = {
"apple": [3, 8, 10],
"banana": [4, 2, 9, 5],
"cherry": [9, 21, 13, 8, 27, 6],
};
assertEquals(
reduceGroups(fruits, (sum, it) => sum + it, 0),
{
"apple": 21,
"banana": 20,
"cherry": 84,
},
);
内部実装は以下のようになっており、mapValuesを呼んでいるだけでした。
export function reduceGroups<T, A>(
record: Readonly<Record<string, Array<T>>>,
reducer: (accumulator: A, current: T) => A,
initialValue: A,
): Record<string, A> {
return mapValues(record, (it) => it.reduce(reducer, initialValue));
}
使用例の一つとして紹介するんじゃなくて関数として提供するんだ…と思いました。確かに使い所はありそうです。
runningReduce
配列と関数を受け取り、先頭から各値までの部分配列にArray.prototype.reduce()を適用した結果をまとめた配列を返します。
日本語の表現が難しい。サンプルコードをご覧ください。
const numbers = [9, 21, 13, 8, 27, 6];
assertEquals(
runningReduce(numbers, (sum, it) => sum + it, 0),
[9, 30, 43, 51, 78, 84],
);
// map + slice + reduce
assertEquals(
numbers.map((_, idx, array) =>
array.slice(0, idx + 1).reduce((sum, it) => sum + it, 0)
),
[9, 30, 43, 51, 78, 84],
);
上記では、引数numbers
の「自分より前の合計」が配列として返されます。「日々の売上と、その日までの累計売上」みたいなイメージでしょうか。
同様の動作をmap
とslice
とreduce
の組み合わせで書いてみましたので、比較してみてください。
lispでいうとmaplist
…でしたっけ(うろおぼえ)
sample
来ました。配列からランダムに一つピックアップする関数です。需要ありますよね…。
標準ライブラリで提供してくれるのはありがたいです。
const numbers = [9, 21, 13, 8, 27, 6];
const random = sample(numbers);
assert(random !== undefined && numbers.includes(random));
返り値の型がundefined
の可能性(引数の配列が空配列の場合)があるので、使用の際はその点のチェックが必要になります。
slidingWindows
配列を与えられた要素数ごとにまとめられた二次元配列に変換します。
RubyでいうとEnumerable#each_consです。
オプションが指定できるので、それぞれについて解説します。
配列と要素数のみを指定すると、その要素数で、先頭を1つずつずらした部分配列の配列が返ります。
const numbers = [1, 2, 3, 4, 5];
assertEquals(
slidingWindows(numbers, 3),
[
[1, 2, 3],
[2, 3, 4],
[3, 4, 5],
],
);
step
オプションを使うことで、先頭のシフト量を指定できます。
const numbers = [1, 2, 3, 4, 5];
assertEquals(
slidingWindows(numbers, 3, { step: 2 }),
[
[1, 2, 3],
[3, 4, 5],
],
);
partial
オプションを使うと、最後のほうの部分配列が短くなる場合でも省略せずに返します。
const numbers = [1, 2, 3, 4, 5];
assertEquals(
slidingWindows(numbers, 3, { partial: true }),
[
[1, 2, 3],
[2, 3, 4],
[3, 4, 5],
[4, 5],
[5],
],
);
引数として渡す窓サイズとstep
の値を同じものにし、partial
オプションも指定すると、chunkと同じ結果が得られます。
const numbers = [1, 2, 3, 4, 5];
assertEquals(
slidingWindows(numbers, 3, { partial: true, step: 3 }),
[
[1, 2, 3],
[4, 5],
],
);
assertEquals(
chunk(numbers, 3),
[
[1, 2, 3],
[4, 5],
],
);
withoutAll
配列をふたつ受け取り、前者に含まれて後者に含まれない要素を抜き出します。
差分処理ですね。
以下のように、後者の配列に含まれる要素は前者から全て除去されます。
const fruits = [
"apple",
"banana",
"cherry",
"kiwi",
"apple",
"banana",
];
assertEquals(
withoutAll(fruits, ["apple", "kiwi"]),
[
"banana",
"cherry",
"banana",
],
);
intersectやunionと合わせ、集合関係のツールが充実した感じです。
その他
非公開のユーティリティ関数の追加
_utils.ts
に、randomInteger
という関数が追加されています。
sample
で使うために追加されたものであり、mod.ts
では公開されていませんが、_utils.ts
を直接読み込めば使用できます。
簡単なランダム整数がほしいときに使えるかもしれません。
既存の関数の改名
上記のslidingWindows
の例にちらっと含めていますが、chunked
がchunk
に改名されています。
これけっこう破壊的ではないかと思うのですが、リリースノートでは触れられていませんでした。
既存のコード内で使用していた方はご注意ください。
おわりに
今回は大量に関数が追加されました。
collections
自体がかなり大きく育ってきているので、関数を使用する場合、mod.ts
ではなく、個々のファイルを直接読み込んだ方が良いかもしれません。
普通に実行する場合はDenoのキャッシュが効くので気にならないと思いますが、CIなどで毎回読み込み直す場合、collection
全体をダウンロードせずに済みます。
collections
の関数追加予定は以下のissuesで取りまとめられています。
本記事執筆時点では、まだやり残しがあるようです。
今後もしばらく増強が続くと思われます。
Discussion
執筆お疲れ様です、いつも参考にさせて頂いてます。
記事中いくつか「述語」が「術後」になっている部分があるようです。
ご指摘感謝します 修正しました