culage
@culage

Are you sure you want to delete the question?

If your question is resolved, you may close it.

Leaving a resolved question undeleted may help others!

We hope you find it useful!

イベントドリブンのプログラムで逐次処理を行うには、どうやればいいか?

Discussion

やりたいこと

ブラウザ上のjavascriptのようなイベントドリブン型のプログラムが動作する環境で、逐次処理を行いたいと思っています。
例えば以下のような処理を行いたいと考えています。

1: 「1番目の数字を入力してください。」と表示し、キー入力を待つ。
2: 「2番目の数字を入力してください。」と表示し、キー入力を待つ。
3: 「結果です。何かキーを押してください。」と表示と、1番目と2番目の数の合計を表示し、キー入力を待つ。
4: 1に戻る。

前提条件として、以下の制約があります。
・プログラム実行環境のjavascriptバージョンが古いため、async/awaitは利用できません。
・入力はイベントから受け取りたいため、promptなどを利用してモーダルダイアログから入力を受け取ることはできません。

質問

イベントドリブンのプログラムで、後述の「switchで処理を分割する方法」のような問題を回避してスマートに逐次処理を記述する、うまい方法はないでしょうか?

async/awaitを利用する例(目的システムでは使えない)

async/awaitを利用すれば以下のようにスマートに記述ができますが、前述のようにasync/await目的システムでは使えません。

See the Pen Sequential processing (async/await) by culage (@culage) on CodePen.

switchで処理を分割する方法

他の方法としては以下のように泥臭くswitchで処理を分割すればで一応実現は出来ます。
しかしこの方法は、変数スコープが無駄に広くなってしまう、ループ処理などが発生したときの対応が困難、といった問題があります。

See the Pen Sequential processing (async/await) by culage (@culage) on CodePen.

0

対象の言語はJSではないということでしょうか。JSであればasync/awaitが使えなくてもpromiseに...とまず思います。

グローバル変数でステートを記載したりせず、callbackの形にしたり再起処理の形にしてみてはいかがですか?

1Like

@tonberry1050さん、回答ありがとうございます。
言語はJavaScriptではありません。(javascriptに似た文法を持つローカルな言語Promiseは使えません)

0Like

なにかまだ悩んでいるようなので再起処理での簡単なサンプルを作りました。
messageのあたりとか雑な場所が多いですが、再起の部分以外は本筋ではないので大目に見てください。(特にJS的にはonkeypressじゃなくてadd/removeEventListenerを使って欲しいです)

 function waitInput(values) {
   const getValueText = v => v == null ? '?' : v;
   const message = [
     '1番目の数字を入力してください',
     '2番目の数字を入力してください',
     null,
     '結果が出ました、何かキーを押してください',
   ][values.length];
   lblMsg.innerText = message;
   lblResult.innerText = `${getValueText(values[0])} + ${getValueText(values[1])} = ${getValueText(values[2])}`;
   
   switch(values.length) {
     case 0: // 入力
       document.body.onkeypress = e => waitInput([e.key]); 
       break;
     case 1: // 入力と計算
       document.body.onkeypress = e => waitInput([values[0], e.key, Number(values[0]) + Number(e.key)]);
       break;
     case 3: // リセット
       document.body.onkeypress = e => waitInput([]); 
       break;
   }
 }
 
 waitInput([]);

追記:
なんだこの汚いlength!っと思ったら引数にステートを脳内補間してください

1Like

連結リストといい感じに結果を表示する関数でどうでしょう。
switchの方が分かりやすい気がしないでもないですが。

const msgs = [
  '1番目の数字を入力してください。',
  '2番目の数字を入力してください。',
  '結果です。何かキーを押してください。',
];
let current;
function Delay(msg){
  this.run = () => {
    current = this;
    document.getElementById('msg').innerHTML = msg;
    Delay.result();
  };
}
let nums = [];
Delay.init = () => {
  let delays = msgs.map(msg => new Delay(msg));
  delays.reduce((prev, curr) => prev.next = curr);
  return delays[0];
};

Delay.result = () => {
  var as = msgs.slice(0, msgs.length - 1).map((_, i) => nums[i] !== undefined ? nums[i] : '?');
  var sum = msgs.length == nums.length + 1 ? nums.reduce((s, a) => Number(s) + Number(a), 0) : '?';
  document.getElementById('result').innerHTML = as.join(' + ') + ' = ' + sum;
};

Delay.handleKeyPress = e => {
  if (current.next){
    nums.push(e.key);
    current.next.run();
  }else{
    nums = [];
    first.run();
  }
};


document.body.onkeypress = Delay.handleKeyPress;
const first = Delay.init();
first.run();
1Like

Javascriptの制限を知らないので的外れかも知れませんが、状態変数を定義し、イベント発生時に状態変数に応じて処理を選択し、処理後に状態変数をあたらしい値に更新すれば、プログラミングがどのような言語や仕組みの上で動いていても通用するのではないかと思います。
 この例であれば、第一の数の入力待ち状態、第二の数の入力待ち状態、何らかのキー入力待ち状態の3状態ですから、状態変数(stat)は、整数型で3つの状態(数値とするよりも、数値を割り当てた状態名にするとわかりやすい)をとり、初期値は、第一の数の入力待ち状態(例えばstat0 値は0)とします。
 イベント発生時には、statを参照して(case)分岐し、処理後に新たな状態(例えばstat1 値は1)を状態変数statに代入するという方法です。
 非同期の通信で、コマンドと結果のやりとりをするようなプログラムで活用していました。WindowsマシンとPLCの間の通信です。この装置の場合、コマンドの処理時間が区々で、コマンド送出は必ずしも先行するコマンドへのレスポンスが完了していない段階でも次々と行うため、単純な状態管理では済まず、コマンドキュー毎に進捗ステータスを管理します。その中にはタイムアウトも含まれています。タイマー処理で残留コマンドをすべてスキャンし、タイムアウトの管理をしながら、レスポンスキューも調べるというようなやり方です。本質問とは無関係ですが、相当複雑な処理も、「状態変数」や、「キュー」を使って作ることが可能です。

1Like

@tonberry1050さん、ありがとうございます。
再帰的に処理を呼び出す部分は「switchで処理を分割する方法」に近いですが、
クロージャと、変数を引数として受け渡して維持することを利用して、利用する変数を関数内に閉じ込める手法が参考になりました。

@nishimuraさん、ありがとございます。
なるほど、各処理状態で行うことをオブジェクトにしてそれを連結リストにした感じですね。
このアイディアは思いついていなかったので参考になりました。
ただこの方式だとループや条件分岐に対応するのが難しいと感じます。

@SShinsukeさん、ありがとうございます。
質問文内の「switchで処理を分割する方法」に近い方法ですね。
実際に利用されたことのあるとのことで、安心感を得られました。

0Like

状態変数によって実行する処理を切り替えていく手法は「ステートマシン」というもので、広く通じる名前がついているほどポピュラーなものです。
ループや条件分岐にも対応させたいのなら「javascript ステートマシン」で検索して参考にできそうなものを探すといいと思います。

こういう目的のためにyieldという便利なキーワードもあるんですが、async/awaitが使えないということはたぶん使えないだろうなぁ

0Like

@albireoさん、ありがとうございます。
ステートマシンやStateパターンは処理の流れの見通しが悪くなってしまうので、ちょっと求めているものとは違いますかね。
実行環境でyieldは使えませんが、C#ではyieldは内部的にswitchで実現されている (https://qiita.com/mrngsht/items/399a67e42c91978e38d1) ようなのでやはり質問文に書いた「switchで処理を分割する方法」がベターな気がしてきました。

0Like

JSの話になりますが、async-await>promise>callbackは全て等価に置き換え可能です。callbackはまず検討してみましたでしょうか?

この質問は突き詰めると「JSのpromiseのように書きたい」→「JSの仕様と同じくイベントループを実装してPromiseも実装すればいいじゃん!」となってしまいそうです。

ステートマシンは嫌だなど返答を見ているに、やはり要するにイベントとか関係なく操作画面(クリックイベント)とビジネスロジックを分けたい、というどの設計でもある話が本題なのかなと思いました。
でしたらOnClick(state)関数を作り、その中でifでもswitchでも配列の添字でもなんでもいいから分岐させ、対応するロジックの関数(私のサンプルならonkeypressの部分)を呼べば良いだけです。

ステートの変数のスコープを狭めたいという話とイベントドリブン(イベント駆動)というのは話が違うのではないかなと思います。
サンプルに書いたように場合によっては引数や呼び出す関数の変更で間に合わせられる可能性はありますが、皆が参照する変数やオブジェクトのスコープがその広さになるのは仕方ないと思います。

1Like

@tonberry1050さん、ありがとうございます。
やはり処理を分岐させて現在ステップ(ステート)に対応するロジックを呼び出すといったところに落ち着きそうですね。
皆様の意見、色々と参考になりました。ありがとございました。

0Like

Your answer might help someone💌