Deno標準ライブラリ0.102.0で追加されたcollectionsの紹介
先日、Deno標準ライブラリのバージョン0.102.0が公開されました。
こちらで追加されたcollectionsというモジュールを紹介します。
その名の通り、集合の扱いを支援してくれるモジュールです。
なお、READMEに載っているサンプルコードは記述にミスがあるのでそのままでは動作しません。
PRが出ているのでそのうち解決されると思います。
本記事では先にRecord
用のものを紹介し、あとからArray
用のものを紹介します。
また、紹介順序も辞書順ではなく、説明をしやすいように調整しています。
したがって公式ページのREADMEとは関数説明の順番が揃っていませんのでご注意ください。
filterRecord系
Array.prototype.filter()のような操作をRecord
に対して行えます。
なかなか便利そうです。
filterKeys
key
でフィルタリングします。
const fruits = {
apple: 25,
banana: 20,
cherry: 84,
kiwi: 34,
lemon: 56,
melon: 90,
orange: 42,
peach: 87,
};
assertEquals(
filterKeys(fruits, (key) => key.length === 6),
{
banana: 20,
cherry: 84,
orange: 42,
},
);
filterValues
value
でフィルタリングします。
const fruits = {
apple: 25,
banana: 20,
cherry: 84,
kiwi: 34,
lemon: 56,
melon: 90,
orange: 42,
peach: 87,
};
assertEquals(
filterValues(fruits, (value) => value > 50),
{
cherry: 84,
lemon: 56,
melon: 90,
peach: 87,
},
);
filterEntries
[key, value]
のペアでフィルタリングします。
const fruits = {
apple: 25,
banana: 20,
cherry: 84,
kiwi: 34,
lemon: 56,
melon: 90,
orange: 42,
peach: 87,
};
assertEquals(
filterEntries(fruits, ([key, value]) => key.length === 6 && value > 50),
{ cherry: 84 },
);
filterKeys
やfilterValues
と比べると出番が少ないかも…?
mapRecord系
Array.prototype.map()のような操作をRecord
に対して行えます。
mapKeys
key
に対して処理を行います。
const fruits = {
apple: 25,
banana: 20,
cherry: 84,
kiwi: 34,
};
assertEquals(
mapKeys(fruits, (key) => "my_" + key),
{
my_apple: 25,
my_banana: 20,
my_cherry: 84,
my_kiwi: 34,
},
);
これはちょっと活用が難しいかな…?キーを書き換えたいときってパッとイメージできませんね。
mapValues
value
に対して処理を行います。
const fruits = {
apple: 25,
banana: 20,
cherry: 84,
kiwi: 34,
};
assertEquals(
mapValues(fruits, (value) => value += 15),
{
apple: 40,
banana: 35,
cherry: 99,
kiwi: 49,
},
);
これは活躍の機会が多そうです。
mapEntries
[key, value]
のペアに対して処理を行います。
第二引数の関数は第一要素を文字列とした配列で返す必要があります。
const fruits = {
apple: 25,
banana: 20,
cherry: 84,
kiwi: 34,
};
assertEquals(
mapEntries(fruits, ([key, value]) => [`${value}`, key]),
{
25: "apple",
20: "banana",
84: "cherry",
34: "kiwi",
},
);
こんな感じで「キーとバリューを入れ替える」ことも簡単にできます。
これは便利そう。
chunked
配列を任意の個数ごとにまとめられた二次元配列に変換します。
元の配列の要素数が与えられた個数で割り切れない場合、最後の個数が揃わなくなります。この場合もエラーは出さずその状態で返却されます。
RubyのEnumerable#each_sliceみたいな感じですね。
const fruits = [
"apple",
"banana",
"cherry",
"kiwi",
"lemon",
"melon",
"orange",
"peach",
];
assertEquals(chunked(fruits, 2), [
["apple", "banana"],
["cherry", "kiwi"],
["lemon", "melon"],
["orange", "peach"],
]);
assertEquals(chunked(fruits, 3), [
["apple", "banana", "cherry"],
["kiwi", "lemon", "melon"],
["orange", "peach"],
]);
distinct
配列から重複する要素を取り除きます。
こちらもRubyの例を出すと、Enumerable#uniqのような感じでしょうか。
重複するものは先に出たものが残り、後のものが取り除かれます。
const numbers = [3, 2, 5, 2, 5];
assertEquals(distinct(numbers), [3, 2, 5]);
内部的には入力された配列をSetにして、それをArray.from()しているようです。
したがって、要素が配列などの場合は重複除去は行われません。
const fruits = [
["apple", "banana"],
["apple", "cherry"],
["apple", "banana"],
];
assertEquals(
distinct(fruits),
[
["apple", "banana"],
["apple", "cherry"],
],
);
// error: Uncaught AssertionError: Values are not equal:
// [Diff] Actual / Expected
// [
// [
// "apple",
// "banana",
// ],
// [
// "apple",
// "cherry",
// ],
// - [
// - "apple",
// - "banana",
// - ],
// ]
distinctBy
distinct
の要素比較方法を自分で設定できる、より一般的なバージョンです。
第2引数として受け取った関数の返り値を一意にする配列を返します。
例えば、文字数が一通りになるように単語を抽出するとか(使い所は不明)。
const fruits = [
"apple",
"banana",
"cherry",
"kiwi",
"lemon",
"melon",
"orange",
"peach",
];
assertEquals(
distinctBy(fruits, (fruit) => fruit.length),
["apple", "banana", "kiwi"],
);
distinct
とは違い、各要素が配列等の場合も対応可能です。
下記でJSON.stringify
での比較が適切かはさておき、distinct
との違いを見てください。
const fruits = [
["apple", "banana"],
["apple", "cherry"],
["apple", "banana"],
];
assertEquals(
distinctBy(fruits, (fruits) => JSON.stringify(fruits)),
[
["apple", "banana"],
["apple", "cherry"],
],
);
設計に関する個人的な意見としては、自分だったらdistinct
にオプショナル引数を追加する形で一緒にしてしまうかなあ…と思いました。
findLast
Array.prototype.find()と似ていますが、述語関数を満たす最後の要素を返します。
const fruits = [
{ name: "apple", count: 25 },
{ name: "banana", count: 20 },
{ name: "cherry", count: 84 },
{ name: "kiwi", count: 34 },
];
assertEquals(
findLast(fruits, (fruit) => fruit.count < 50),
{ name: "kiwi", count: 34 },
);
Array.prototype.findIndex()はあるのでfindLastIndex
があっても良さそうですが、今のところは無いみたいですね[1]。
groupBy
配列を第二引数(セレクター関数)の結果に応じてグループ分けします。
日本語だとちょっと説明が難しいので、具体的なコードを示します。
const fruits = [
{ name: "apple", count: 25 },
{ name: "banana", count: 20 },
{ name: "cherry", count: 84 },
{ name: "kiwi", count: 34 },
];
assertEquals(
groupBy(fruits, (fruit) => `${fruit.count % 3}`),
{
"0": [
{ name: "cherry", count: 84 },
],
"1": [
{ name: "apple", count: 25 },
{ name: "kiwi", count: 34 },
],
"2": [
{ name: "banana", count: 20 },
],
},
);
ここでは、fruit.count % 3
の結果で要素をグループ分けしています。
なお、結果がRecord
になる関係上、セレクター関数の返り値は文字列となる必要があります。
intersect
入力された配列の共通部分(全てに存在する要素)の配列を返します。
const fruits1 = ["apple", "banana", "cherry"];
const fruits2 = ["cherry", "kiwi", "peach"];
const fruits3 = ["apple", "cherry", "peach"];
assertEquals(
intersect(fruits1, fruits2, fruits3),
["cherry"],
);
partition
渡された術後関数に基づいて配列から要素を抽出します。
Array.prototype.filter()と異なる点は、「条件を満たす要素」と「条件を満たさない要素」のペアとして返してくれるところです。
const fruits = [
{ name: "apple", count: 25 },
{ name: "banana", count: 20 },
{ name: "cherry", count: 84 },
{ name: "kiwi", count: 34 },
];
assertEquals(
partition(fruits, (fruit) => fruit.count > 50),
[
[
{ name: "cherry", count: 84 },
],
[
{ name: "apple", count: 25 },
{ name: "banana", count: 20 },
{ name: "kiwi", count: 34 },
],
],
);
返り値の第一要素が条件を満たしたもの、第二要素が条件を満たさなかったものとなります。
permutations
与えられた配列の要素で作られる順列を返します。
const fruits = [
"apple",
"banana",
"cherry",
];
assertEquals(
permutations(fruits),
[
["apple", "banana", "cherry"],
["banana", "apple", "cherry"],
["cherry", "apple", "banana"],
["apple", "cherry", "banana"],
["banana", "cherry", "apple"],
["cherry", "banana", "apple"],
],
);
現時点では「n個の要素を持つ配列からk個選ぶ」といったことはできず、必ずすべての要素が使われるようです。つまりn個の要素を持つ配列を渡すと長さがnの階乗である配列が返ってきます。
ここは抽出個数を選べるとありがたいので、機能追加を期待したいところです[2]。
隠し要素?
現バージョン(0.102.0)では、公開されているのにREADMEに載っておらず、mod.ts
内でもexport
されていない項目が存在します。
未だ正式リリースできないという判断なのか、それとも単に忘れられているのかは不明です。
mod.ts
から読み込むことはできませんが、以下のように直接参照すれば使用できます。
import { union } from "https://deno.land/std@0.102.0/collections/union.ts";
union
複数の配列をまとめ、重複した要素を取り除きます。
const fruits1 = ["apple", "banana", "cherry"];
const fruits2 = ["cherry", "kiwi", "peach"];
const fruits3 = ["apple", "cherry", "peach"];
assertEquals(
union(fruits1, fruits2, fruits3),
["apple", "banana", "cherry", "kiwi", "peach"],
);
なお、「配列をまとめて重複を除去する」なので、前述のdistinct
を使っても書くことができます。
assertEquals(
distinct([...fruits1, ...fruits2, ...fruits3]),
union(fruits1, fruits2, fruits3),
);
distinct
を使うよりはunion
を使ったほうが簡潔です。
ただ、重複条件を自分で定義したい場合はdistinctBy
を採用する場合もあるかもしれません。
zip
2つの配列を引数に取り、「それらの同一インデックスの要素の組」の配列を返します。
RubyのEnumerable#zipと類似していますが、Rubyのそれと違い、受け取れる配列は2つのみです。
配列の長さが異なる場合、最も小さいサイズのものに合わせられます。言い方を変えると、はみ出したぶんは考慮されません。
const fruitNames = ["apple", "banana", "cherry", "kiwi"];
const fruitCounts = [25, 20, 84, 34, 999];
assertEquals(
zip(fruitNames, fruitCounts),
[
["apple", 25],
["banana", 20],
["cherry", 84],
["kiwi", 34],
],
);
unzip
上記のzip
の逆関数です。タプルの配列を各要素の配列に分解します。
const fruits: [string, number][] = [
["apple", 25],
["banana", 20],
["cherry", 84],
["kiwi", 34],
];
assertEquals(
unzip(fruits),
[
["apple", "banana", "cherry", "kiwi"],
[25, 20, 84, 34],
],
);
上記のfruits
は、Typescriptの型推論で(string | number)[][]
と推定されます(内部のペアの型の順序・要素数までは指定されません)が、これではunzip
で扱うことができません。
各要素が2要素の配列で、それらの型も決まっていることを明示する必要があります。
おわりに
いくつかRubyの例を出しましたが、「他の言語ならこの処理を簡単に書けるのにな…」というものをまとめてくれた印象です。
このモジュールを活用することで、コード内から中間変数やループを減らすことができ、見通しの向上に繋がりそうです。関数型っぽく書ける場合も増えそうですね。
chunked
などは同じようなものを自作して使った経験があるのですが、Deno公式で提供してくれるのはありがたいです。
Discussion