この資料はCHIRIMEN for Raspberry Pi3を使った講習会向けのTipsです。
ワンデー講習会などでJavascriptの初歩を勉強したところでCHIRIMEN for Raspberry Pi用のプログラムを読むと、よくわからない部分があると思います。.thenというキーワードが出てきて無名関数が入れ子になっているところだと思います。これは非同期処理という特別な処理方法に因るものです。
CHIRIMEN for Raspberry Piでのプログラミングでは、センサーやアクチュエータを使いますが、これらは動作するのに無視できない時間がかかります。例えば、体温計の測定やロボットハンドの腕の動きを想像してみてください。非同期処理はこのような動作に時間がかかる処理において、処理の完了を待って次の処理を行うときによく使われます。(非同期処理無しに待とうとすれば、その間ブラウザ画面がフリーズしてしまいます。)
非同期処理のプログラムにはいくつもの書き方があり、統一されていません。また、入れ子が深かったり、無名関数が使われていたりして、読み解くのが難しくもあります。CHIRIMENの講習ではasync awaitを用いた方法に統一して説明をするようにしています。関数の入れ子を作らずに非同期処理が書け、基本的な文法を学んだ方にとって比較的わかりやすい記法のためです。
- 注記1: async awaitはかなり新しいjavascriptの文法です。CHIRIMEN for Raspberry Pi3をはじめ新しいウェブブラウザは既にサポートしています
- 注記2: 非同期処理はセンサーやアクチュエータを使った処理固有のものではありません。例えばサーバに問合せを行い返答を待機する時やキーの入力を待機する などのときにも使われます。webアプリでよく使われる処理です。
ここでは、このようなasync await以外の記法を使った非同期処理のプログラムを読み解き、 ここで紹介した async接頭辞を持つ関数(async await処理) で、入れ子のないプログラム構造に翻訳する方法を説明します。
注記:書き換え前後で厳密には同じ処理でない部分がありますが、ここでは省略します。
まず、キーワード .then を見つけます。 .thenの前には関数の呼び出しがあるはずです。この関数が非同期処理を行っている関数です。 そして.thenの次から関数の入れ子(ネスト)になっているはずです。
CHIRIMEN for Raspberry Pi 3 Hello Worldを例にすると、
- navigator.requestGPIOAccess()
- port.export("out") の二つが非同期処理を行っている関数です。
この部分は大抵次のいずれかのパターンになっています。
- 次の処理に変数を渡すもの
- 非同期処理関数(パラメータ).then(function(変数名){
- 非同期処理関数(パラメータ).then(変数名=>{
- 変数を渡さないもの
- 非同期処理関数(パラメータ).then(function(){
- 非同期処理関数(パラメータ).then(()=>{
これらが、非同期処理の完了を待って、無名関数で定義した内容を実行する ということをしています。なお、=>はアロー関数式というもので上の行の省略記法です。
これらを、次のように書き換えます。
- プログラムコード全体をasync接頭辞をつけた関数で包む
- 次の処理に変数を渡すもの
- var 変数名 = await 非同期処理関数(パラメータ);
- 変数を渡さないもの
- await 非同期処理関数(パラメータ);
- thenの前についているreturnは気にしない
それでは CHIRIMEN for Raspberry Pi 3 Hello World のコードを書き換えてみましょう。
元のソースコード:
navigator.requestGPIOAccess().then(gpioAccess=>{
var port = gpioAccess.ports.get(26);
var v = 0;
return port.export("out").then(()=>{
setInterval(function(){
v ^= 1;
port.write(v);
},1000);
}); // 実際はこの部分も入れ子なのでネストはさらに深い
}); // 同上
関数の入れ子なしに書き換え後のソースコード:
async function mainFunction(){ // まず、プログラム全体をasync関数で包みます。
var gpioAccess = await navigator.requestGPIOAccess(); // thenの前の関数をawait接頭辞をつけて呼び出します。
var port = gpioAccess.ports.get(26);
await port.export("out");
setInterval(function(){
v ^= 1;
port.write(v);
},1000);
}
mainFunction(); // 定義したasync関数を実行します(このプログラムのエントリーポイントになっています)
またsetInterval関数も、thenを使わないもっと古いタイプの非同期処理関数だったりします。 setIntervalは指定されたmsの間隔を置いて、入れ子になっている関数を無限に呼び出すものです。 そのため これも関数の入れ子でわかりにくいかもしれません。 そこで、指定した時間(ms)処理を待たせる非同期処理関数(sleep関数)とwhile文による無限ループを使って、入れ子のない形に書き換えてみます。
setIntervalの入れ子もなしにしたソースコード
// sleep関数の定義方法はとりあえず気にしなくて良いです。コピペしてください。
// 単に指定したms秒スリープするだけの非同期処理関数
// noprotect
function sleep(ms){
return new Promise( function(resolve) {
setTimeout(resolve, ms);
});
}
async function mainFunction(){ // まず、プログラム全体をasync関数で包みます。
var gpioAccess = await navigator.requestGPIOAccess(); // thenの前の関数をawait接頭辞をつけて呼び出します。
var port = gpioAccess.ports.get(26);
await port.export("out");
while ( true ){ // 無限ループ
v ^= 1; // v = v ^ 1 (XOR 演算)の意。 vが1の場合はvが0に、0の場合は1に変化する。1でLED点灯、0で消灯するので、1秒間隔でLEDがON OFFする。
port.write(v);
await sleep(1000); // 1000ms待機する
}
}
mainFunction(); // 定義したasync関数を実行します(このプログラムのエントリーポイントになっています)
- 注記1: このような形の非同期処理関数として、setInterval以外にも、setTimeout, XMLHttpRequestなどのブラウザ組み込み関数があります
- 注記2: // noprotect は、jsbinを使ったときに、無限ループ保護機能が働くのを防止するための jsbinのローカルルール(裏技..)です