Deno標準モジュール解説[前編] ~Deno標準モジュールの概要と、モジュール解説(Archive~FMT)

Deno標準モジュールを、前編と後編の2回に分けて解説します。本記事は前編です後編はこちら⁠)⁠。

はじめに

Deno標準モジュールはDenoコアチームによって開発・メンテナンスされているモジュール群です。Denoを使って様々なプログラムを作成する上で必要となる基本的な機能を提供しています。

標準モジュールを使う際には以下の例のようにhttps://deno.land/std名前空間から必要な機能をインポートして使います。たとえば、HTTPサーバーを使用する例は以下のようになります。

import { serve } from "https://deno.land/std@0.170.0/http/server.ts";

serve(() => new Response("hello, world"));
コード 注:@の後ろの数字は標準モジュールのバージョンです。

標準モジュールのカバーする範囲は非常に多岐に渡ります。一例を上げると、ファイルシステム関連、一般的なファイルフォーマットの扱い(YAML、CSV、TOML、JSONCなど⁠⁠、エンコーディング形式(Base64、Base58など)、暗号処理、HTTP関連、CLIツール用のヘルパー、Node.js互換API群等、言語機能をサポートするようなヘルパー群等が標準モジュールに含まれます。

標準モジュールには現在23のカテゴリが登録されています。本記事では各カテゴリを俯瞰的に眺めつつ解説していきます。

Deno標準モジュールのカバー範囲

非常にカバー範囲の広い標準モジュールですが、あらゆる機能が含まれているわけではありません。

標準モジュールでは特定のベンダーのソフトウェアとのインターフェースは提供しないとされています。特定ベンダーのRDBMS(Postgres、MySQL、SQLite)やKVS(Redisなど)などのドライバーは標準モジュールからは提供されません。これらを利用したい場合は3rdパーティツールに頼る必要があります。

また、標準モジュールでは、フレームワーク的な機能は提供しないとされています。ここでいうフレームワークとはRailsのような、特定のステップに従えば迅速にアプリケーションを開発できるタイプのソフトウェアをさします。標準モジュールでは、そのようなアプリケーションの骨格を提供するのではなく、あくまで、アプリケーションのパーツになるような基礎的な機能を提供します。

Node.jsとの比較

Node.jsには標準モジュールに相当する公式のライブラリという概念はありません。Node.js本体に含まれる機能だけが公式のAPIで、それ以外はすべて3rdパーティという関係です。そのため3rdパーティに頼る比重が必然的に多くなります。

それに対してDenoは、本体のAPIがまずあって、次にカバー範囲の広い標準モジュールがあり、それ以外が3rdパーティという構造になっています。

このような構成にする狙いの一つは、よりランタイムを安心して使えるようにすることです。自分のやりたいことが標準モジュールの機能として提供されていれば安心してそれを使えます。3rdパーティのモジュールを使う場合は、そのツールが本当に信頼に足るツールなのかを評価する必要がありますが、そのような評価を正しく行うのは相当に難易度が高いです(大企業が提供するツールが、依存していた3rdパーティツールの問題に巻き込まれて問題を起こすような事案がしばしば起きています⁠⁠。

Python、Ruby、Go、Rustなどの他のメジャーな言語では標準モジュール(ライブラリ)が非常に広い範囲をカバーしていて、このようなカバー範囲の大きい標準モジュール(ライブラリ)という言語の構成は、珍しいものではなく、むしろ当たり前の構成だと言えます。Denoはこのような他の言語で当たり前の構成をとっていると見ることもできます。

エコシステムレベルで考えた場合の標準モジュールの狙いは、巨大な依存ツリーを避けるというものがあります。Node.jsでは多くの機能を3rdパーティに移譲したことで、小さなモジュール群が依存の連鎖を繰り返して依存ツリーが巨大になるという問題がありました。標準モジュールが提供されることで、よくある機能の提供場所が標準モジュールに集約されて、結果として大きすぎる依存ツリーという問題が解消されることが期待されています。

しかし、標準モジュールを持つことはメリットだけではないかもしれません。考えられるデメリットとして、公式以外の選択肢が狭まることがありそうです。たとえば、Node.jsではテスト関連の3rdパーティのモジュール群は選択肢が非常に豊富にあります。Denoの場合は本体にbuiltinされたテストランナーDeno.teststd/testingから提供されるアサーションでほとんど事足りてしまうため、選択肢が狭まっていると言えます(選択肢が少ないことをデメリットと取るかメリットと取るか、人や場合によるかもしれませんが⁠⁠。

モジュール解説

以下ではDeno標準モジュールの中の各モジュールについて解説していきます。

1. Archive

Archiveは書庫形式ファイルの処理が含まれる名前空間です。Archiveには現在のところTar形式の操作用のAPIが実装されています。Tarの圧縮・解凍を行うには以下のように行います。

import { Tar } from "https://deno.land/std@0.170.0/archive/tar.ts";
import { Buffer } from "https://deno.land/std@0.170.0/io/buffer.ts";
import { copy } from "https://deno.land/std@0.170.0/streams/copy.ts";

const tar = new Tar();

// Uint8Arrayからデータを追加
const content = new TextEncoder().encode("Deno.land");
await tar.append("deno.txt", {
  reader: new Buffer(content),
  contentSize: content.byteLength,
});

// 実ファイル(./land.txt)を指定してエントリーを追加
await tar.append("land.txt", {
  filePath: "./land.txt",
});

// Tarアーカイブをファイルに書き出す
const writer = await Deno.open("./out.tar", { write: true, create: true });
await copy(tar.getReader(), writer);
writer.close();

Archiveでは将来的にはZip形式やその他のアーカイブ形式の操作用のAPIが実装される予定です。

2. Async

Asyncディレクトリ以下では、PromiseやAsyncItearatorに関連した非同期処理をサポートするためのユーティリティ類が提供されています。

  • abortable:任意のプロミスにabortする機能を追加する
  • deadline:任意のプロミスに、時間制限を追加する
  • debounce:デバウンス(処理の間引き)機能
  • deferred:外部からresolve/reject可能なプロミスを作るユーティリティ
  • delay:一定時間でresolveするプロミスを作る
  • MuxAsyncIterator:複数のAsyncIteratorを受け取って1つのAsyncIteratorにまとめる
  • pooledMap:複数のプロミスを受け取って一定個数づつ実行し、結果をAsyncIteratorにする
  • retry:与えられた処理を指数バックオフの時間間隔でリトライする
  • tee:AsyncIteratorを任意の個数に分岐する

ここでは例としてdeadlineretryの使い方を紹介します。

deadlineはプロミスと、ミリ秒を受け取って、与えられたミリ秒以内にプロミスがresolve or rejectしなければ、DeadlineErrorでrejectされるようなプロミスを返す関数です。

import {
  deadline,
  DeadlineError,
} from "https://deno.land/std@0.170.0/async/deadline.ts";

try {
  await deadline(fetch("https://example.com/"), 1000);
} catch (e) {
  if (e instanceof DeadlineError) {
    console.error("1秒以内にレスポンスがありませんでした");
  } else {
    throw e;
  }
}

上記の例では、https://example.comにリクエストするプロミスを1秒(1000ミリ秒)の制限時間付きでリクエストする例です。1秒以内にリクエストが完了すれば、後続処理に進んでゆき、1秒以内にリクエストが完了できない場合は、1秒以内にレスポンスがありませんでしたと表示されます。

deadlineは制限時間付きで何らかの非同期処理を実行したい場合に便利な関数です。

次にretryの例を見てみましょう。retryは与えられた処理が成功するまで、規定回数(デフォルトは5回)まで繰り返し実行してくれる関数です。繰り返し実行する際は、いわゆる指数後退(exponential backoff)アルゴリズムで、次の試行までの時間間隔を一定割合で増加させながら実行してくれます。

import { retry } from "https://deno.land/std@0.170.0/async/retry.ts";

await retry(() => fetch("https://my-service.example/"), {
  maxAttempts: 6,
});

上記の例ではhttps://my-service.example/にリクエストするという非同期処理が成功するまで6回まで再試行するという例になっています。6回すべて失敗するとスクリプト自体が失敗します。

retryは、ある程度の不安定性を許容したいような処理を記述したい場合に便利な関数です。

3. Bytes

Bytesではバイナリ処理に関するユーティリティが実装されています。

  • BytesList:複数のバイト列を1つのバイト列のように扱う(主にパフォーマンスチューニング用)
  • concat:複数のバイト列をつなげて1つのバイト列にする
  • copy:バイト列をコピーする
  • endsWith:あるバイト列の終端が、与えられたバイト列と一致するかを判定する
  • equals:2つのバイト列が等しいかどうか判定する
  • includesNeedle:あるバイト列が、与えられたバイト列を内部に含んでいるかどうか判定する
  • indexOfNeedle:あるバイト列が、与えれたバイト列を含んでいる場合にその最初の位置を返す(含んでいなければ-1を返す)
  • lastIndexOfNeedle:あるバイト列が、与えられたバイト列を含んでいる場合にその最後の位置を返す(含んでいなければ-1を返す)
  • repeat:バイト列を指定回数繰り返したバイト列を返す
  • startsWith:あるバイト列の先頭が、与えられたバイト列と一致するかを判定する

ここでは例として、includesNeedlerepeatを使った例を紹介します。

includesNeedleは2つのバイト列を受け取って、1つめのバイト列が2つめのバイト列を含んでいればtrueを返します。なお、3引数目で配列のインデックスを指定すると、与えられたインデックス以降から探します。

import { includesNeedle } from "https://deno.land/std@0.170.0/bytes/includes_needle.ts";

const source = new Uint8Array([0, 1, 2, 1, 2, 1, 2, 3]);
const needle = new Uint8Array([1, 2]);
console.log(includesNeedle(source, needle)); // true
console.log(includesNeedle(source, needle, 6)); // false

repeatは与えられたバイト列を指定回数繰り返したバイト列を返します。なお、繰り返し回数にマイナスの値や、整数でない値を入力すると例外になります。

import { repeat } from "https://deno.land/std@$STD_VERSION/bytes/repeat.ts";

const source = new Uint8Array([0, 1, 2]);
console.log(repeat(source, 3)); // [0, 1, 2, 0, 1, 2, 0, 1, 2]
console.log(repeat(source, 0)); // []
console.log(repeat(source, -1)); // RangeError
console.log(repeat(source, 1.5)); // Error

4. Collections

Collectionsでは配列操作、オブジェクト操作の中で一般性の高いと考えられる処理が実装されています。lodashの配列・オブジェクト操作系の処理と似たような処理を多く含みます。

  • aggregateGroups:すべての値が配列であるようなオブジェクトについて、各値を与えられたコールバック関数で集約する
  • associateBy:配列の要素Vに対して、与えられた関数を用いてキーKを選択し、KをキーにVを値として持つようなオブジェクトを作って返す
  • associateWith:配列の要素Kに対して、与えられた関数を用いて値Vを選択し、KをキーにVを値として持つようなオブジェクトを作って返す
  • BinaryHeap:二分ヒープの実装
  • BinarySearchTree:二分探索木の実装
  • chunk:与えられた配列を与えられた要素数のchunkに切り分ける
  • deepMerge:与えられた2つの配列とオブジェクトのネスト構造を再帰的にマージする
  • distinct:与えられた配列の要素から重複要素を取り除いた配列を返す
  • distinctBy:配列の要素に対して関数を呼び出した結果に重複が無いように配列から要素を間引いた配列を返す
  • dropLastWhile:配列の末尾から各要素に対して関数を呼び出し、最初に結果がfalseになるまでの要素を削除した配列を返す
  • dropWhile:配列の先頭から各要素に対して関数を呼び出し、最初に結果がfalseになるのまでの要素を削除した配列を返す
  • filterEntries:オブジェクトのキーと値のペアに対して与えられた関数を呼び出して、falseになったものを削除したオブジェクトを返す
  • filterKeys:オブジェクトのキーに対して与えられた関数を呼び出して、falseになったものを削除したオブジェクトを返す
  • filterValues:オブジェクトの値に対して与えられた関数を呼び出して、falseになったものを削除したオブジェクトを返す
  • findSingle:配列の要素に対して関数を呼び出して、結果がtrueになるような要素が一つだけ存在した場合に、その要素を返す
  • firstNotNullishOf:配列の要素に対して関数を呼び出した結果がnullish(nullかundefinedでない)にならない最初のものを返す
  • groupBy:配列の要素Xに対して関数を呼び出して、その結果をKとする。Kをキーとして、KがキーとなるようなXを集めた配列を値とするオブジェクトを生成して返す。
  • includesValue:オブジェクトが与えられた値をもつかどうかを判定する
  • intersect:与えられた複数個の配列から共通部分を含む配列を返す
  • joinToString:高機能な文字列の結合
  • mapEntries:オブジェクトのキーと値のペアに対して関数を呼び出してその結果で値を置き換えたオブジェクトを返す
  • mapKeys:オブジェクトのキーに対して関数を呼び出してその結果で値を置き換えたオブジェクトを返す
  • mapNotNullish:配列を与えられた関数でmapし、nullish(nullかundefined)にならなかった要素だけを集めた配列を返す
  • mapValues:オブジェクトの値に対して関数をよびだしてその結果で値を置き換えたオブジェクトを返す
  • maxBy:配列の要素で与えられた関数を呼び出し、その結果の値が最大になるような、元の配列の要素を返す
  • maxOf:配列の要素で与えられた関数を呼び出し、その結果の値が最大になる時の、結果の値を返す
  • maxWith:配列の要素を与えられた2引数比較関数で比較した際に、最大になるような、元の配列の要素を返す
  • minBy:配列の要素で与えられた関数を呼び出し、その結果の値が最小になるような、元の配列の要素を返す
  • minOf:配列の要素で与えられた関数を呼び出し、その結果の値が最小になる時の、結果の値を返す
  • minWith:配列の要素を与えられた2引数比較関数で比較した際に、最小になるような、元の配列の要素を返す
  • partition:配列の要素を与えられた関数で呼び出して、trueなったものの配列と、falseになったものの配列からなる2要素の配列を返す
  • permutation:配列の要素を並び替えた列のすべての組み合わせからなる配列を返す
  • RedBlackTree:クラス、赤黒木の実装
  • reduceGroups:値が配列であるオブジェクトの各値を与えられた関数でreduceする。
  • runningReduce:配列を与えられた関数でreduceする際の計算途中の値全体の配列を返す
  • sample:配列からランダムに1要素取得する
  • slidingWindows:配列から、与えられた要素数の部分配列を取り出し、その取り出す操作を先頭から1要素づつずらしながら行い、その様にして生成した部分配列全体の配列を返す
  • sortBy:配列の要素に対して関数を呼びその結果の値にもとづいて要素をソートする
  • sumOf:配列の要素に対して関数を呼びその結果の値の合計値を返す
  • takeLastWhile:配列の要素に対して末尾から順に関数を呼び出し、最初に値がfalseになるまでの間の要素を取り出した配列を返す
  • takeWhile:配列の要素に対して末尾から順に関数を呼び出し、最初に値がfalseになるまでの間の要素を取り出した配列を返す
  • union:複数の配列の和の配列を返す
  • unzip:2-タプルの配列を受け取って、タプルの1要素目だけの配列と、2要素目だけの配列のタプルを返す
  • withoutAll:与えられた配列から、与えれた複数の要素をすべて削除した配列を返す
  • zip:複数の配列を受け取って、先頭から1要素ずつ取り出してタプルを作成し、その様なタプルの配列を返す

各機能の詳細は公式ドキュメントを参照してください。

5. Crypto

CryptoではWeb Crypto APIの独自拡張などの、暗号処理関連機能が実装されています。

Web Crypto APIの中のdigestが特に拡張されていて、SHA-3系、BLAKE2系、BLAKE3系のアルゴリズム等がサポートされています。

import { crypto } from "https://deno.land/std@0.170.0/crypto/mod.ts";

const a = await crypto.subtle.digest(
  "SHA3-224",
  new TextEncoder().encode("hello world"),
);

なお、SHA-256などのWeb Cryptoでサポートされているアルゴリズムが指定された場合は、内部的にWeb Cryptoの呼び出しに置き換わります。

また、Web Crypto APIのdigest関数はArrayBufferもしくはTypedArrayのみをサポートしていますが、このdigestはAsyncIterableの入力も受け付けているため、大きなデータをストリーミングしながらdigest値を得ることができます。

import { crypto } from "https://deno.land/std@0.170.0/crypto/mod.ts";

const file = await Deno.open("data.txt");
const a = await crypto.subtle.digest("BLAKE3", file.readable);
file.close();

他に、digestSyncという同期的なAPIも用意されています。こちらのAPIは主にNode互換性に必要というモチベーションで追加されたAPIですが、どうしても同期的にdigest値を得体場面で有用かもしれません。

import { crypto } from "https://deno.land/std@0.170.0/crypto/mod.ts";

const a = await crypto.subtle.digestSync(
  "SHA3-224",
  new TextEncoder().encode("hello world"),
);

さらにCryptoモジュールでは、タイミング攻撃を防ぐ手段として有効な、timingSafeEqual関数なども提供されています。クレデンシャルの比較時など、タイミングからの情報漏洩が気になる場面ではこの関数が便利です。

import { timingSafeEqual } from "https://deno.land/std@0.170.0/crypto/timing_safe_equal.ts";

const a = new TextEncoder().encode("a".repeat(1000));
const b = new TextEncoder().encode("b".repeat(1000));
timingSafeEqual(a, a);
timingSafeEqual(a, b); // 上の比較と同じ時間がかかる

6. Datetime

Datetimeでは日付に関する処理が実装されています。具体的には以下のような関数が提供されています。

  • dayOfYear:年初からの日数を返す
  • difference:2つの日付の差をオブジェクト形式で返す
  • format:日付を与えられたフォーマット形式に整形する
  • isLeap:日付の年が閏年であるかどうかを判定する
  • parse:与えられた文字列を与えられたフォーマット形式で解釈し、その日付を返す
  • toIMF:与えられた日付をIMF形式に整形する

ここでは例として、formatparseを使う例を紹介します。

import {
  format,
  parse,
} from "https://deno.land/std@0.170.0/datetime/format.ts";
format(new Date(2019, 0, 20), "yyyy-MM-dd"); // => "2019-01-20"
parse("2019-01-20", "yyyy-MM-dd"); // => new Date(2019, 0, 20)

なお、現在JSの標準化団体TC39では、次世代の日付表現のオブジェクトとしてTemporalの策定が進んでいます。Temporalは提案としてのステージは現在3であり、近い将来に新たなJSの仕様となる可能性が高く、DenoのJSエンジンであるV8でも既に実装がある程度進んでいます。将来的にTemporalでカバーされる機能についてはstdからは提供されなくなると想定されており、現在のDatetimeモジュールの機能の多くが将来的にdeprecated(非推奨)になる可能性が高いです。非推奨になる場合は、その理由がJSDocの@deprecatedタグに記載され、代わりにどの様な手段を使えば良いのかが案内されます。その指示に従ってTemporalを利用した書き方に追従していきましょう。

7. Dotenv

Dotenvでは.envファイルから環境変数を読み込む機能が実装されています。

たとえば以下のような.envファイルを作ります。

FOO=bar
HELLO=world

このファイルをカレントディレクトリに置いた状態で、以下のスクリプトを作成実行してみましょう。

import "https://deno.land/std@0.170.0/dotenv/load.ts";
console.log(Deno.env.get("FOO"));
console.log(Deno.env.get("HELLO"));

実行すると以下のようになり、.envファイルから環境変数が読み込まれていることが確認できます。

$ deno run --allow-env --allow-read script.js
bar
world

.envファイルの中では、他の環境変数を${}という記法で参照することもできます。

FOO=bar
BAZ=${FOO}-123

上記の例ではBAZの値はbar-123となります。

.envファイルで環境変数を管理する手法は、Denoに限らずアプリケーション開発で広く使われています。

8. Encoding

Encodingでは各種エンコーディング方式のエンコーダーデコーダー(もしくはパーサー・シリアライザ)が実装されています。

現在は以下のような形式がサポートされています。

  • ASCII85
  • Base32
  • Base58
  • Base64
  • Base64 URL
  • binary
  • CSV
  • フロントマター(マークダウンの先頭にYAML等が埋め込まれた形式)
  • hex
  • JSON Lines、NDJSON(改行で区切られたJSONのストリーム形式)
  • JSONC
  • TOML
  • VARINT
  • YAML

例としてBase64とYAMLを使う例を紹介します。

import {
  decode,
  encode,
} from "https://deno.land/std@0.170.0/encoding/base64.ts";
const data = "aGVsbG8=";
const binaryData = decode(data);
console.log(binaryData);
// => Uint8Array(5) [ 104, 101, 108, 108, 111 ]
console.log(new TextDecoder().decode(binaryData));
// => hello
console.log(encode(binaryData));
// => aGVsbG8=
import {
  parse,
  stringify,
} from "https://deno.land/std@0.170.0/encoding/yaml.ts";

const data = parse(`
foo: bar
baz:
  - qux
  - quux
`);
console.log(data);
// => { foo: "bar", baz: [ "qux", "quux" ] }

const yaml = stringify({ foo: "bar", baz: ["qux", "quux"] });
console.log(yaml);
// =>
// foo: bar
// baz:
//   - qux
//   - quux

9. Flags

Flagsではコマンドラインオプションをパースする関数が提供されています。

例えば以下のようなオプション体系を持ったコマンドを考えてみましょう。

Usage: my-command [-h|--help] [-n|--number <num>] [--dry-run] <target>

Options:
  -h, --help         ヘルプメッセージを表示
  -n, --number <num> 実行回数を指定
  --dry-run          実行せず、実行の計画のみを表示する
  <target>           実行の対象

このオプションを実現するためには、以下のようにflagsモジュールを使います

import { parse } from "https://deno.land/std@0.170.0/flags/mod.ts";

const args = parse(Deno.args, {
  boolean: ["help", "dry-run"],
  string: ["number"],
  alias: {
    h: "help",
    n: "number",
  },
});

const help = args.help;
const number = +args.number!;
const dryRun = args.dryRun;
const target = args._[0];

以上の呼び出しで、helpnumberdryRuntargetなどのパラメータが上記のヘルプメッセージの通りに取得できます。より詳細なparse関数の使い方については、公式ドキュメントを参照してください。

なお、このモジュールはnpmモジュールのminimistをベースにデザインされています。minimistでサポートされているオプション・機能はほぼサポートされていますが、TypeScriptとの親和性を上げるために、一部の挙動がminimistから変更されています。

flagsモジュールはシンプルなコマンドラインツールの作成には十分な機能を備えていますが、例えばgitコマンドのようなサブコマンドがたくさんあり、サブコマンドごとに全く異なるオプション体系を持つようなツールを開発する場合には、機能が足りないと感じる場合もあるかもしれません。その様な場合は3rdパーティモジュールのyargsを使うことをお勧めします。yargsは以下のように使うことができます。

import yargs from "https://deno.land/x/yargs@v17.6.2-deno/deno.ts";
import { Arguments } from "https://deno.land/x/yargs@v17.6.2-deno/deno-types.ts";

yargs(Deno.args)
  .command("download <files...>", "download a list of files", (yargs: any) => {
    return yargs.positional("files", {
      describe: "a list of files to do something with",
    });
  }, (argv: Arguments) => {
    console.info(argv);
  })
  .strictCommands()
  .demandCommand(1)
  .parse();

10. FMT

FMTでは各種フォーマティング(データ整形)用のユーティリティが実装されています。以下のような種類のデータの整形関数が提供されています。

  • バイトサイズの整形
  • ターミナルでの色付け
  • 時間間隔表現の整形
  • printf(C言語のprintf、sprintf互換のフォーマッティング)

バイトサイズのフォーマットの例は以下のようになります。ファイルサイズの出力などの際に便利です。

import { format } from "https://deno.land/std@0.170.0/fmt/bytes.ts";
// バイトサイズを人間に読みやすい形式にする
format(1337);
//=> '1.34 kB'
format(100);
//=> '100 B'
// 単位をbitにすることも出来る
format(1337, { bits: true });
//=> '1.34 kbit'

ターミナルに色を付けたい場合は以下のようになります。CLIツールなどで成功・失敗などの特別な情報を見やすく表示するために使います。

import { green, red } from "https://deno.land/std@0.170.0/fmt/colors.ts";

console.log(red("赤"));
console.log(green("緑"));

時間間隔を見やすく表示します。

import { format } from "https://deno.land/std@0.170.0/fmt/duration.ts";
// "0d 0h 1m 39s 674ms 0µs 0ns"
format(99674);
// "00:00:01:39:674:000:000"
format(99674, { style: "digital" });
// "1 minutes, 39 seconds, 674 milliseconds"
format(99674, { style: "full", ignoreZero: true });

後編に続く

Deno標準モジュールの解説は後編に続きます。また、後編ではDeno標準モジュールの今後の展望についても取り上げます。

おすすめ記事

記事・ニュース一覧