Node.js に変わる Deno という選択肢・その2

前回の続きです。

前回記載した点だけでも Deno を使うメリットとしては十分ですが、今回はそれに加えていくつか Deno の特徴を紹介します。

個人的に嬉しかった Deno の特徴

  • 非同期関数は標準で Promise を利用する
  • 実行時に必要な権限を付与する

サンプル

今回はファイル入出力を行うサンプルコードを例に説明します。
以下 csv を読み込んで json 形式で出力し、最後に完了メッセージを出力するものとします。
エラーが起きた場合は内容に関わらず処理を中断させます。

  • 入力として利用するファイル (input.csv)
1,hoge
2,fuga
3,piyo

  • 期待する出力結果 (output.json)
[{"id":"1","name":"hoge"},{"id":"2","name":"fuga"},{"id":"3","name":"piyo"}]

非同期関数は標準で Promise を利用する

非同期処理について、Node.js は原則コールバックで処理します。
前述のようなファイル入出力を行う場合、以下のようなコードになります。

// parse_csv_to_json.js
const fs = require('fs');

fs.readFile('./input.csv', (inputErr, inputData) => {
  if (inputErr) {
    console.error(inputErr);
    return;
  }
  const outputData = inputData.toString()
    .split('\n')
    .filter(v => !!v)
    .map((v) => {
      const [id, name] = v.split(',');
      return { id, name };
    });
  fs.writeFile('./output.json', JSON.stringify(outputData), (outputErr) => {
    if (outputErr) {
      console.error(outputErr);
      return;
    }
    console.log('Completed');
  });
});

readFile writeFile ともに非同期処理なので、それぞれ読み込み/書き込みが終わった(もしくはエラーになった)という結果の判定や後処理はコールバックで行うことになります。
これの何が辛いかというと、処理が増えるほどコールバックが連鎖していくことになるということです。

  • 出力処理は入力処理が終わってから実施する必要がある
  • よって出力処理は入力処理のコールバックに書く必要がある(1つ目のコールバック)
  • 出力処理が終わった際の完了メッセージは出力処理のコールバックに書く必要がある(2つ目のコールバック)

今回は簡単な処理なので2つしかありませんが、処理が増えるほどこのコールバック連鎖は増えていくので、ソースコードとしては読みにくいものになりがちです。
また、コールバック内でエラーハンドリングも行う必要があるため、そこでハンドリングが漏れた場合に意図せぬデータを使って後続処理が実行される可能性もあります。

そういった問題を解決するため、後発で Promise という仕組みが生まれました。
Node.js でも Promise は使えるものの、標準・外部問わず一部のライブラリではコールバックが前提になっているものも多いです。
また、今回のようにトップレベルで利用する場合、色々と制約があります(mjs にする必要があるとか node のバージョンを一定以上にするとか)。

Deno ではこの仕組みを標準で採用しているため、コードがかなりシンプルになります。
前述の Node.js と同様の処理をする場合、Deno では以下のようなコードになります。

// parse_csv_to_json.ts
try {
  // read* や write* は Promise 型を返す
  // 処理完了を待機する場合は await を使う
  const inputData = await Deno.readTextFile("./input.csv");
  const outputData = inputData
    .split("\n")
    .filter((v) => !!v)
    .map((v) => {
      const [id, name] = v.split(",");
      return { id, name };
    });
  await Deno.writeTextFile("./output.json", JSON.stringify(outputData));
  console.log("Completed");
} catch (err) {
  // 途中でエラーになった場合は処理を中断してここに到達する
  console.error(err);
}

めちゃめちゃシンプルになりました。
コールバックと違い try-catch でシンプルに例外処理をハンドリングできるのも良いところです。
もちろん、アプリケーションによってはエラーが起きる処理毎に try-catch するなどで多少複雑になることはありますが、それでもコールバックの連鎖に比べると正常系・異常系が分かれる分かなり見やすくはなると思います。

ただ、このファイルを Deno で実行する際は一工夫必要です。

実行時に必要な権限を付与する

先のコードを実行してみると以下のようにエラーが出力されるかと思います。

$ deno run parse_csv_to_json.ts --allow-read --allow-write
PermissionDenied: Requires read access to "./input.csv", run again with the --allow-read flag
~以下省略~

Deno は、ファイルシステムやネットワークへのアクセスが標準では許可されていません。
よってファイル実行時に都度許可してやる必要があります。
今回のコードだと、ファイル入出力を行うので --allow-read--allow-write が必要です。

$ deno run --allow-read --allow-write parse_csv_to_json.ts 
Check file:///path/to/parse_csv_to_json.ts
Completed

これがメリットかどうかは賛否両論ありそうですが、セキュリティーホールを突かれないため最低限のアクセスのみ許可するというのはアプリケーションとして正しい姿だと思います。
また、アプリケーションが利用する外部リソースを把握したうえで実行できる(コード改修の結果不要になったらフラグを消せば良い)というのも良いところだと思っています。

まとめ

かなりざっくりとですが、個人的に嬉しかった Deno の特徴についてまとめました。
Node.js を長く使ってきたこともあって私自身最初は色々混乱した部分もありましたが、今は簡単なツールを作る場面などで積極的に使うようにしています。
いずれは Deno を使ったアプリケーションも作りたいと思っています。
VSCode の設定についても書きたいと思いますが、それはまたの機会に。