はじめに

Nodeで対話的プログラムを作る時、少し本格的なものなら、prompt をインストールして使えばよいし、お手軽にやりたければ readline モジュールを使う。

しかし、どちらにせよ不満がある、なので筆をとった。いったい何が不満なのか?

コールバックはお嫌いですか?

話を少し変えよう。コールバックは皆嫌いだと思うし私も嫌いだ。なぜ嫌いか?それはコントロールフローの問題だ。

何かのリソースを取得する非同期関数を呼び出すと、フローが二つに分かれる。一つは関数呼び出し元のフローで、もう一つは、リソースが実際に取得された際に実行されるフローだ。前者をメインのフロー、後者をコールバックのフローと便宜的に呼ぼう。

getSomeResource((resource) => {
  // do some cool stuff here
  console.log('ここはコールバックのフロー');
});

console.log('ここがメインのフロー');

プログラマは、この二つのフローを相手にしなければならない。非常に煩雑だ(だからこそ効率のよい処理もできるのだが・・・・) await が好まれるのは、二つのフローを同時に相手にする必要がなくなるという点が大きい。

const resource = await getSomeResourcePromise(); // コールバックのフローが終わるのを待つ
console.log('メインのフロー')

コールバックのフローとメインのフローを同時に相手にする。この構造が対話プログラム作成時にも出てくるのだ。

対話プログラムの構造

コンソールから二つの数字を受け取って、合計値を返すというシンプルなプログラムを考えよう。readline モジュールを使うとこう書ける。

const readline = require('readline');

const reader = readline.createInterface({ input: process.stdin });

const numbers = [];
reader.on('line', (line) => {
  const num = parseInt(line, 10);
  if (num) numbers.push(num);
  if (numbers.length !== 2) return;
  reader.close();

  // インプットが終わった時の処理
  const sum = numbers[0] + numbers[1];
  console.log(`sum is ${sum}`);
});

// ★ ほんとはここに合計の処理を書きたい

前章で指摘したコントロールフローの問題があるのがわかる。reader のコールバック内に、インプットが終わった後の処理を書く必要がある。本来であれば、合計値を出すといったロジックはメインのフローに書くべきであるにもかかわらず。

これは、prompt を使う場合でも同様だ。

const prompt = require('prompt');

prompt.start();
prompt.get(['num1', 'num2'], (err, result) => {

  // インプットが終わった時の処理
  const sum = parseInt(result.num1, 10) +
    parseInt(result.num2, 10);
  console.log(`sum is ${sum}`);
});

// ★ ほんとはここに合計の処理を書きたい

「メインのフローには何も書かずに、コールバックのフローに色々書けば?」といわれるかもしれないが、これはコールバック地獄と呼ばれるので好ましいものとみなされない。

解決の方向性

コールバックの時と同じで、await を使ってコントロールフローをシンプルにするのは好ましい方向であるように考える。つまりこのようにしたいのだ。

// メインのフローで以下のようにしたい
const numbers = await somePromise;
const sum = numbers[0] + numbers[1];
console.log(`sum is ${sum}`);

さて、これをどう実装しようか?

一つの解法

こうするとうまくいく。

const readline = require('readline');

const onLine = (line, reader, resolve, numbers) => {
  const num = parseInt(line, 10);
  if (num) numbers.push(num);
  if (numbers.length !== 2) return;
  reader.close();
  resolve(numbers); // 入力が終わったら resolve する
};

(async () => {
  const numbers = [];
  const reader = readline.createInterface({ input: process.stdin });
  const promise = new Promise(r => reader.on('line', line => onLine(line, reader, r, numbers)));
  const result = await promise;
  const sum = result[0] + result[1]; // メインのフローに処理をかけた
  console.log(`sum is ${sum}`);
})();

ぱっと見、より複雑になったように見えるが、見慣れてくればより分かりやすく見える(少なくとも私には)。

技術的には、行処理ごとに呼び出されるコールバックにPromise のリゾルバを操作させるという発想だ。

まとめ

非同期関数のコールバック地獄が await によって解消されるように、対話プログラムにおける入力に関しても await を使うことで見通しが良くなることができるということを示した。

Sign up for free and join this conversation.
Sign Up
If you already have a Qiita account log in.