🦕

Deno標準ライブラリ0.107.0で増強されたcollectionsの紹介

2021/09/15に公開
2

Deno標準ライブラリのバージョン0.107.0が公開されました。

https://github.com/denoland/deno_std/releases/tag/0.107.0

こちらで増強されたcollectionsというモジュールを紹介します。
https://deno.land/std@0.107.0/collections

その名の通り、集合の扱いを支援してくれるモジュールです。

本記事では0.107.0のリリースで追加された関数を紹介します。
既存のものは以下の記事で解説していますので、あわせてご覧ください。
https://zenn.dev/kawarimidoll/articles/4ea4219cf69225
https://zenn.dev/kawarimidoll/articles/7d1fc9f0fb6538

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,
);

後ろから走査しているので、takeWhiledropWhileを組み合わせた場合とは順序が逆になります。

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,
);

distinctdistinctByみたいに、比較関数を調整するマイナーチェンジ版が出そうな予感…。

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の「自分より前の合計」が配列として返されます。「日々の売上と、その日までの累計売上」みたいなイメージでしょうか。
同様の動作をmapslicereduceの組み合わせで書いてみましたので、比較してみてください。

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",
  ],
);

intersectunionと合わせ、集合関係のツールが充実した感じです。

その他

非公開のユーティリティ関数の追加

_utils.tsに、randomIntegerという関数が追加されています。
sampleで使うために追加されたものであり、mod.tsでは公開されていませんが、_utils.tsを直接読み込めば使用できます。
簡単なランダム整数がほしいときに使えるかもしれません。

既存の関数の改名

上記のslidingWindowsの例にちらっと含めていますが、chunkedchunkに改名されています。
これけっこう破壊的ではないかと思うのですが、リリースノートでは触れられていませんでした。
既存のコード内で使用していた方はご注意ください。

おわりに

今回は大量に関数が追加されました。
collections自体がかなり大きく育ってきているので、関数を使用する場合、mod.tsではなく、個々のファイルを直接読み込んだ方が良いかもしれません。
普通に実行する場合はDenoのキャッシュが効くので気にならないと思いますが、CIなどで毎回読み込み直す場合、collection全体をダウンロードせずに済みます。

collectionsの関数追加予定は以下のissuesで取りまとめられています。
https://github.com/denoland/deno_std/issues/1173
本記事執筆時点では、まだやり残しがあるようです。
今後もしばらく増強が続くと思われます。

Discussion

kuukuu

執筆お疲れ様です、いつも参考にさせて頂いてます。
記事中いくつか「述語」が「術後」になっている部分があるようです。