JavaScriptで構文解析:npm Lex-BNF で任意の言語を定義する
photo credit: Morton1905 Austria. Wien. Pieter Breugel d. Ä. Oil on oak panel, 114 x 155 cm. via photopin (license)
JavaScriptで、テキストの構文を定義して、その文法に従った解析、評価を支援する npm モジュール lex-bnf を正式にリリースしましたのでご紹介。 ユーザー入力のコマンドや計算式の解釈など、ちょっとしたテキストの解析器を実装するために使えますよ。
特徴
- いわゆるコンパイラコンパイラではありません。JavaScriptでテキストの構文解析を行い、JavaScriptで評価を行うためのモジュールです。
- 構文定義は BNF(バッカス=ナウア記法)的に行います。
- 再帰下降パーサーによって構文解析。
- 構文解析に先立って基本的な字句解析(identifier, number-literal, punctuator, and white-space)。
- 再帰下降パーサーによる解析時に、無限再帰に陥ってしまう「左再帰」問題には、繰り返し指定子を導入することで解決しています。
- 構文定義に合わせて、ユーザーの評価関数を定義します。評価関数によってインタプリタはモチロン、中間言語へのコンパイラも記述できます(今の所、コンパイラの支援機能はありません)
構文定義方法
以降で構文定義の方法を説明してみますが、詳細はかなり長くなりそうなので、雰囲気だけ掴んでみてください(徐々に加筆します)
基本
言語の構文はlex-bnf
がエクスポートする Language
クラスのインスタンスとして定義します。
このクラスのコンストラクタで、以下3つのstaticプロパティを使用して、BNFのようなデータ構造を記述します。
syntax
- 文法を定義するための関数。戻り値は構文の「項(term)」を表します。第一引数に項の名称(文字列)、第二引数では、項の名称(=非終端記号)か終端記号の二次元配列によって構文のルールを定義します。第三引数には規則の条件によっては省略可能な評価関数(後述)を指定します。終端記号は次の2つのプロパティで定義します。項の名称は別の文法を参照し、構文定義内に含まれていなくてはなりません。literal
- 文字リテラル(定数)の定義するための関数です。第一引数にアルファベットとアンダースコアだけを含む任意の長さの文字列を指定します。数字や区切り文字を含ませてはいけません(そういえば関数内でチェックしてませんわ汗)。numlit
- 数字リテラルの終端記号を表す定数。数字リテラルは数字だけを含む任意の長さの文字列です。
※ 簡潔に記述するために、これらプロパティは独立した定数としてスプレッド構文で取り出しておいたほうが便利です。
calc.js
: - 四則演算式の文法定義(冒頭部分を抜粋;全体は下の方に掲載)
"use strict"; const Language = require("lex-bnf"); const {syntax, literal: lit, numlit} = Language; // Defines syntax of language by BNF-like definition and evaluator. const calc = new Language([ syntax("calc", [["expression"]]), // The evaluator is omittable when the rules contains only one // term such as `additive-expression` below. syntax("expression", [["additive-expression"]]), // (略)
繰り返し記号
規則に現れる項の名前の末尾に*
をつけると、その項が繰り返されることを意味します。
項の繰り返しは、項を再帰的に参照することで定義できますが、規則の左端で再帰を行う左再帰では、構文解析時に無限再帰に陥りますので使えません(概念としては左再帰で定義可能)。 規則の右端で再起する右再帰では式の評価順序が「右から左」となってしまいますので、式によっては正しい結果が得られません。
このため、左再帰が必要である場合は、繰り返し記号を使用して以下のように定義しなくてはなりません。
以下のサンプルでは左再帰を回避するために繰り返し記号を使っています。
// 省略 syntax("additive-expression", [ ["multiplicative-expression", "additive-expression-rest*" /* ← */ ], ], (term) => { /*評価関数(中略)*/}), syntax("additive-expression-rest", [ [lit("+"), "multiplicative-expression"], [lit("-"), "multiplicative-expression"], ], (term) => term.contents()), syntax("multiplicative-expression", [ ["unary-expression", "multiplicative-expression-rest*"], ], // 省略
// 省略 syntax("additive-expression", [ ["additive-expression", "multiplicative-expression"], ["multiplicative-expression"], ], (term) => { /*評価関数(中略)*/}), syntax("multiplicative-expression", [ ["multiplicative-expression", "unary-expression"], ["unary-expression"], ], // 省略
以下は右再帰の例。 左から右へ評価される計算式のような構文では、式によっては正しい計算結果が得られない場合があります。 あまりないと思いますが、右から左へ評価される文法では問題なし。
// 省略 syntax("additive-expression", [ ["multiplicative-expression", "additive-expression"], ["multiplicative-expression"], ], (term) => { /*評価関数(中略)*/}), syntax("multiplicative-expression", [ ["multiplicative-expression", "unary-expression"], ["unary-expression"], ], // 省略
参考
サンプル:四則演算計算機
シンプルな計算機を実装したサンプルプログラムです。 コマンドラインから指定した計算式の結果を表示します。
以下2つのソースファイルで構成しています。
どちらのファイルもリポジトリの sample
ディレクトリにあります。
ここではこれらを使って、構文定義の方法を説明しています。
calc.js
- 四則演算の構文定義。eval-expr.js
- 四則演算計算機
実行するには、git リポジトリをclone してから node sample/eval-expr.js '<式>'
とします。
式はシングルコーテーションでくくったほうが良いです。Bashで *
や括弧が特別な意味を持っていますので。
実行例)
~ $ cd Git ~/Git $ git clone https://github.com/takamin/lex-bnf.git ~/Git $ cd lex-bnf ~/lex-bnf $ node sample/eval-expr.js '(1 + 2) * (3 - 4) / 5' -0.6
calc.js
- 四則演算の構文定義
このファイルで、+
、-
、*
、/
と括弧が使える四則演算式の構文定義を行っています。
"use strict"; const Language = require("lex-bnf"); const {syntax, literal: lit, numlit} = Language; // Defines syntax of language by BNF-like definition and evaluator. const calc = new Language([ syntax("calc", [["expression"]]), // The evaluator is omittable when the rules contains only one // term such as `additive-expression` below. syntax("expression", [["additive-expression"]]), syntax("additive-expression", [ // The trailing `*` is a repetition specifier. // It can avoid an infinite recursion leading from // left-recursion. ["multiplicative-expression", "additive-expression-rest*"], ], /** * evaluate 'additive-expression' * @param {Term} term result of parsing 'additive-expression' * @return {number} A result of the calculation. */ (term) => { // get no whitespace tokens const terms = [].concat(...term.contents()); let acc = terms[0]; for(let i = 1; i < terms.length; i += 2) { const ope = terms[i]; const value = terms[i + 1]; switch(ope) { case "+": acc += value; break; case "-": acc -= value; break; } } return acc; }), syntax("additive-expression-rest", [ [lit("+"), "multiplicative-expression"], [lit("-"), "multiplicative-expression"], ], (term) => { return term.contents(); }), syntax("multiplicative-expression", [ ["unary-expression", "multiplicative-expression-rest*"], ], (term) => { const terms = [].concat(...term.contents()); let acc = terms[0]; for(let i = 1; i < terms.length; i += 2) { const ope = terms[i]; const value = terms[i + 1]; switch(ope) { case "*": acc *= value; break; case "/": acc /= value; break; } } return acc; }), syntax("multiplicative-expression-rest", [ [lit("*"), "unary-expression"], [lit("/"), "unary-expression"], ], (term) => { return term.contents(); }), syntax("unary-expression", [["postfix-expression"]]), syntax("postfix-expression", [["primary-expression"]]), syntax("primary-expression", [ ["literal"], [lit("("), "expression", lit(")")], ], (term) => { const terms = term.contents(); return (terms[0] !== "(" ? terms[0] : terms[1]); }), syntax("literal", [ ["floating-constant"], ["integer-constant"], ]), syntax("floating-constant", [ ["floating-real-part", lit("e"), "integer-constant"], ["floating-real-part"], ], (term) => { // Get a token representing this element const s = term.str(); // With the BNF rule above, the string could include white spaces. // If the string includes white spaces, this method throws an error. // The error will be returned from `Language#evaluate()`. if(/\s/.test(s)) { throw new Error("invalid text for floating-constant"); } return parseFloat(s); }), syntax("floating-real-part", [ // The optional term is not offerd, so all patterns should be declared. [numlit, lit("."), numlit], [numlit, lit(".")], [lit("."), numlit], ["sign", numlit, lit("."), numlit], ["sign", numlit, lit(".")], ["sign", lit("."), numlit], ], (term) => term.str()), syntax("integer-constant", [ [numlit], ["sign", numlit], ], (term) => { return parseInt(term.str()); }), syntax("sign", [ [lit("+")], [lit("-")], ], (term) => term.str()), ]); module.exports = calc;
eval-expr.js
- 四則演算計算機
"use strict"; const calc = require("./calc.js"); const expr = process.argv[2]; const result = calc.parse(expr); if(result.error) { console.error( `Syntax error: stopped at ${JSON.stringify(result.errorToken)}`); } else { try { const value = calc.evaluate(result); console.log(`${value}`); } catch(err) { console.error(`Evaluation error: ${err.message}`); } }
リンク
意外な結果:MapとObjectの速度を比較
photo credit: hehaden D is for dry via photopin (license)
ObjectとMapの速度を比較をしてみたんですが、予想外の結果を得ましたのでご報告します。
JavaScript で Key-Valueマップ(=ディクショナリ)を操作する場合、今まで何の疑いもなく object でやっていました。 しかし、ES6 で Mapクラスというのが追加されていて、Key-Valueマップとしては、こちらを使うのがオススメだそうなんです。 MDNのリファレンスでは、「(MapとObjectを比較すると)いくつかの場面で Map の方が勝るような重要な違いがあります」と書かれています。 ほぼ同じことがObjectで実現できるのに、なにが違っているのだろう? そして、パフォーマンス的にはどっちが有利なんだろう?といった疑問が出てきたので、検証用のプログラムを書いて確認してみました。
※ 以降で紹介している検証は Node.js v14.15.1 (lts/fermium) で行いました。自宅の貧弱PCでの検証です。別の環境だと違う結果になるかも知れませんのでご留意を。
[ゲーム&モダンJavaScript文法で2倍楽しい]グラフィックスプログラミング入門——リアルタイムに動く画面を描く。プログラマー直伝の基本 WEB+DB PRESS plus
杉本 雅広(著)
新品 ¥2,905 18個の評価
Amazon.co.jpで詳細を見る
MapとObjectの比較検証
ObjectとMapのインスタンスに対して、以下の内容の操作を行い、その処理速度を比較します。
- Key-Valueの挿入(Insert) - 新たなキーを複数回挿入する。
- Valueの上書き(Update) - 既存のキーの値を更新する。
- Valueの取得(Get) - 特定のキーを指定して値を取り出す。
- Keyの列挙(Keys) - 内包するキーをすべて列挙する。
- Valueの列挙(Values) - 内包する値をすべて列挙する。
- Key-Valueの削除(Delete) - 既存のキーを削除する
また、キーの型によって優劣に差が出るかも知れないと思い、とりあえず「文字列をキーにした場合」と、「数値をキーにした場合」の両方で検証しました。
検証用のプログラム
ちょっと長いですが、上記の内容をゴリゴリっと書いた結果がコレ。 もう少し短くできると思ったのですが、途中で諦めた。
"use strict"; const ListIt = require("list-it"); class TestRun { elapse; constructor(x, keys, handler) { const t0 = Date.now(); handler(x, keys); const t1 = Date.now(); this.elapse = t1 - t0; } } const TEST_AMOUNT = 2000000; const STRING_KEYS = Array(TEST_AMOUNT).fill(null).map((_, i) => `STR-${i}`); const NUMBER_KEYS = Array(TEST_AMOUNT).fill(null).map((_, i) => i); const tests = [ { name: "insert", operation: { obj: (obj, keys) => keys.forEach((key, i) => obj[key] = i), map: (map, keys) => keys.forEach((key, i) => map.set(key, i)), }, }, { name: "update", operation: { obj: (obj, keys) => keys.forEach((key, i) => obj[key] = i * 2), map: (map, keys) => keys.forEach((key, i) => map.set(key, i * 2)), }, }, { name: "get", operation: { obj: (obj, keys) => keys.forEach((key, i) => obj[key] = obj[key] * 2), map: (map, keys) => keys.forEach((key, i) => map.set(key, map.get(key))), }, }, { name: "keys", operation: { obj: (obj, keys) => Object.keys(obj), map: (map, keys) => map.keys(), }, }, { name: "values", operation: { obj: (obj, keys) => Object.values(obj), map: (map, keys) => map.values(), }, }, { name: "delete", operation: { obj: (obj, keys) => keys.forEach((key, i) => delete obj[key]), map: (map, keys) => keys.forEach((key, i) => map.delete(key)), }, }, ]; const targetNames = tests.map(test => test.name); const transpose = (arr) => { const rows = Math.max(...arr.map(arr1 => arr1.length)); const tArr = Array(rows).fill(null).map(()=>(Array(arr.length).fill(0))); arr.forEach((arr1, row) => { arr1.forEach((data, col) => { tArr[col][row] = data; }); }); return tArr; }; const obj1 = {}; const map1 = new Map(); const testObjStr = tests.map(test => (new TestRun(obj1, STRING_KEYS, test.operation.obj))); const testMapStr = tests.map(test => (new TestRun(map1, STRING_KEYS, test.operation.map))); const resultS = transpose([ ["operation", ...targetNames], ["object", ...testObjStr.map(result => result.elapse)], ["Map", ...testMapStr.map(result => result.elapse)], ]); const listitS = new ListIt({headerBold: true, headerColor: "green", headerUnderline: true}); console.log("Key=string"); console.log(listitS.setHeaderRow(resultS.shift()).d(resultS).toString()); console.log(""); const obj2 = {}; const map2 = new Map(); const testObjNum = tests.map(test => (new TestRun(obj2, NUMBER_KEYS, test.operation.obj))); const testMapNum = tests.map(test => (new TestRun(map2, NUMBER_KEYS, test.operation.map))); const resultN = transpose([ ["operation", ...targetNames], ["object", ...testObjNum.map(result => result.elapse)], ["Map", ...testMapNum.map(result => result.elapse)], ]); const listitN = new ListIt({headerBold: true, headerColor: "green", headerUnderline: true}); console.log("Key=number"); console.log(listitN.setHeaderRow(resultN.shift()).d(resultN).toString()); console.log("");
結果発表
上記プログラムを実行すると「キーを文字列にした場合」と「キーを整数にした場合」の2つの表がコンソールに出力されます。 表には、それぞれの操作を行ったときのミリ秒単位の時間を表示しています。 さて、それぞれについて見ていきましょう。
※ キーや値の列挙操作以外は、二千万回の操作をしている時間ですが、ループのオーバーヘッドがあるので純粋な処理時間ではありません。 なので以下の内容で「差がある」と書いていても、それほど大きな違いではありません。
キーを文字列にした場合
コンソール出力:
Key=string operation object Map --------- ------ ---- insert 3888 1558 update 388 846 get 398 1086 keys 2452 0 values 4381 0 delete 936 1127
パッと見感想:不思議な結果ですが、何度やり直してもこれぐらいの数値になりますので、そういうことなんでしょう。
個別の操作について:
- 挿入 - Mapの勝ち。Objectへのキーの挿入はMapの倍以上の時間がかかる。
- 更新 - Objectの勝ち。Mapより3倍近く速い。
- 取得 - Objectの勝ち。更新操作の時間も含まれているので、1行上の時間を減じると object: 10ms, Map: 240ms となり、実質的に24倍!!!
- Key列挙 - Mapの勝ち。Objectめちゃ遅い。Mapはおそらくイテレータを包有しているのでしょう。
- Value列挙 - Mapの勝ち。Objectめちゃめちゃ遅い。Mapはおそらく(同上)
- 削除 - Objectの勝ち。接戦だけど優位な差がある。
キーを整数にした場合
コンソール出力:
Key=number operation object Map --------- ------ ---- insert 244 1161 update 83 501 get 105 515 keys 530 0 values 54 0 delete 426 694
パッと見感想:一部を除いてObjectが爆速。内部的に配列として実装されているのではないか?
個別の操作について:
- 挿入 - Objectの勝ち。Mapの4倍以上の速度。
- 更新 - Objectの勝ち。Mapの6倍以上の速度。
- 取得 - Mapの勝ち。実質的に object: 22ms, Map: 14ms となりますので、ほとんど差はありませんがMapの逆転勝ち。
- Key列挙 - Mapの勝ち。Objectがやはり遅いが、文字列をキーとした場合より4倍以上は頑張っている。
- Value列挙 - Mapの勝ち。Objectが遅いけど、文字列をキーとした場合よりは極端に速い。
- 削除 - Objectの勝ち。接戦だけど優位な差がある。
結果まとめ
ObjectとMapのどちらを使うかと迷ったら、処理内容に応じて、なんとなく以下の順番で検討すれば良いのかなと思いました。
- キーや値を列挙する操作が多い場合 → Mapが優位。
- キーが整数値 → Objectが優位。
- あらかじめキーと値を設定しておき更新や参照をする場合には、Objectが優位。
- 削除の操作は大差なし。
MDNの「キー付きコレクション」というページには以下の文面があります。
Map と Object のどちらを使用すべきかを決めるには下記の 3 つのヒントが役立つでしょう :
- 実行時までキーが不明なとき、またはすべてのキーが同じ型、すべての値が同じ型のときは Object よりも Map を使用しましょう。
- プリミティブ値をキーとして保存する必要がある場合に Map を使用しましょう。Object はキーが数値、真偽値、もしくはいずれのプリミティブ値であるかどうかに関わらず、それぞれのキーを文字列として扱います。
- 個々の要素を操作するロジックがある場合は、Object を使用しましょう。
「実行時までキーが不明なときはMap」というのはObjectへのInsertが遅いためですね。これはわかります。 しかし、「プリミティブ値をキーとして保存するならMap」というのが検証結果とは多少ニュアンスが違っていますが、キーを整数にしたときのObjectの速さは魅力ですね。 まんべんなくいろんな操作をしているのであれば、勝敗表的には引き分けですから、MDNの推奨通りMapを使えばよいのではないでしょうか。
ただし、これは、あくまでも 現在の Node.js v14.15.3 で、自宅PCで実行した結果に基づくものです。 別のプラットフォーム(ブラウザとか)だと別の結果になるかも知れませんからご注意ください。
参考ページ
JavaScriptのヤヤコシイとこ
会社ではJavaScriptおじさん的になっています。主にNode.jsでバックエンドですが、軽めのフロントエンドもたまにやります。
そんなJavaScriptで、このおじさん。最近ちょくちょくやらかしています。 若い人に間違いを指摘されたりしてお恥ずかしい。 少なからず迷惑もかけております。
そんなこんなで、誠心誠意、懺悔と言い訳&最終的にはJavaScriptへの愚痴になるけど、「JavaScriptのこういうところがヤヤコシイですー」ってところを書いておきます。 JavaScriptは大好きですけどヤヤコシイのだ。
空文字列は0である?
先日、以下のようなコードを書いて、思った通りに動いて無くて困りました。
const a = ""; if(a == 0) { console.log("値はゼロ"); } else { console.log("値は空文字列"); }
変数 a
を 空文字列で初期化して「aは0である」という条件判定をしています。
これ、直感的には「偽」ですが、 JavaScriptでは「真」になります。
つまり上のプログラムを実行すると「値はゼロ」と表示されます。
これを正しく判定するには=
を3つ書いて a === 0
としなくてはなりません。
const a = 0; if(a === 0) { console.log("値はゼロ"); } else { console.log("値は空文字列"); }
やってみましょう。以下は Node.js のREPLでの実行結果です。
コンソール(Bashやコマンドプロンプト、パワーシェルなど)を開いて node
と打てば対話型のインタプリタ(REPL)が起動します。
行頭が >
となっている行の右の文字列を入力して[Enter]
で一行ずつ実行できます。
$ node Welcome to Node.js v12.18.2. Type ".help" for more information. > let a = 0 undefined > a 0 > a == "" true > a === "" false >
これはStrict モードでも同じです。REPLをStrictモードで開くには node --user_strict
で起動します。
型に寛容なJavaScriptの世界ではかなり有名で、自分もしっかり意識しているつもりでした。 が、ついうっかり「==」と書いてしまったんですね。 こういった「ついうっかり」を防ぐためにもユニットテストを書くべきですが、それはまた別の話として。
NaN(Not A Number)は数値なの?
NaN は Not a Number の略です。「数値ではない」という意味です。判定するにはグローバルコンテキストの isNaN
という関数が使えます。
しかし NaN
の型名は number
ですし、コンストラクタは Number
なのです。
何かスッキリしないものが残ります。
> a = NaN NaN > isNaN(a) true > typeof a 'number' > a.constructor.name 'Number' >
まあ、あまり多用する言語要素でないし、使うときはきっちり調べてコーディングするので、非効率ですが問題になりにくいのが救いかも。
Array#reverse は配列自体を変化させる
Array#reverse
は配列を逆順にするためのメソッドです。
しかし自分は「Array#reverseは配列自体を変化させずに、新しい配列を返す」と思いこんでいたのですね。
実際、このメソッドの戻り値は逆順になった配列なのですが、まさか配列自体が逆順になって return this;
されているとは思っていなかった。
「ホントかよ!?」みたいな。
困りものです。
REPLで確認してみると、明らかに配列自体が逆順になっていました。
$ node Welcome to Node.js v12.18.3. Type ".help" for more information. > a = [1,2,3,4] [ 1, 2, 3, 4 ] > a.reverse() [ 4, 3, 2, 1 ] > a [ 4, 3, 2, 1 ] >
ややこしいのは、reverse だけでないのです。reverseのように配列自体を変化させてしまうメソッドや、配列自体は変化させず、別の配列を返してくるメソッドが、Arrayクラスには混在してます。 しかし、メソッド名を見ても、その挙動がわからない。
いちいちリファレンスを参照しないと不安ですよね。 全部覚えちゃうわけにも行きませんし。 こういうところで生産性が下がるのね。
まあ、よく使うものは覚えていますが、体調が悪いときや睡眠不足のときなどにはわけがわからなくなってしまったり。 割と大きな問題かもなと思います。
まとめ
それぞれJavaScriptの歴史的な事情が関わっていると思います。1990年代から2005年ぐらいまではブラウザの添え物的で、プラットフォーム依存がひどかった。
その後、ブラウザ間の実装の違いを吸収するライブラリが開発され、ECMAScriptとしての統一仕様の策定、Ajaxの勃興、Node.jsの登場、HTML5の策定など重要なイベントがたくさん有って、しかも、ほとんど下位互換性を保ちながら進化してきたのです。
こういった歴史を知っているなら「仕方がないね」と思えますけど、今から使おうとする人には敷居が高すぎるんじゃないかなと。 こういった論理的でない挙動が参入障壁になっていたり、JavaScriptのイメージが悪化につながっているのではないだろうかと、要らぬ心配をしてしまう。 特にビギナーに説明するには、それなりの時間を使いますし、心理的に「なんやようわからん」となりがちだろうなと。
プログラミング言語の人気投票的な結果ではJavaScriptは割と上位に来ますし、多く使われているのも事実だとは思います。 なんか良い方法ないですかね。
全部約束 Promise.all - 非同期処理を効率よく並列実行するために
photo credit: EpicTop10.com Promise via photopin (license)
互いに独立した複数の非同期処理は、 Promise.all で待ちましょうね!ってことを書いています。
田中 賢一郎(著)
新品 ¥1,650

Amazon.co.jpで詳細を見る
例えば、複数のREST APIの結果を得たい場合、以下のように書きがちです。 一見問題なく見えますが、処理速度的にちょっと無駄なんですね。
実際ワタシも細かいことを気にせずにザクザクとコードを書いている時はこのように書いていて、あとから見直すことが多々あります。
async function sample() { const response1 = await fetch("http://path.to/rest/api1"); const response2 = await fetch("http://path.to/rest/api2"); }
上のコードでは、 最初のAPIの結果を得てから次のリクエストを行います。つまり順次実行されるのですが、これってちょっと無駄なんです。
APIの数が多くなればなるほど処理速度はどんどん伸びます。しかし、そのほとんどが結果を待っているだけの時間なので無駄なんです。 (他の非同期処理が行われるため一概に無駄とは言い切れませんが)
無駄なく実行するコード
複数の独立した非同期処理を素早く実行するには、以下のように書きます。
async function sample() { const [response1, response2] = await Promise.all([ fetch("http://path.to/rest/api1"), fetch("http://path.to/rest/api2"), ]); }
仮にAPIの数が10個になると、最初の例では約10倍の時間がかかります。でも Promise.all
を使った場合は、それほど時間は伸びません。
理論的には、ひとつのAPIの結果を得るまでの時間と同程度のはず(実際にはRESTサーバー側の処理能力に依存しますが)。
Promise.all
について
Promise.all
は、複数のPromiseオブジェクトがすべて完了するのを待ちます。
引数はPromiseオブジェクトの配列で、ひとつのPromiseオブジェクトを返します。
Promise.all
が返すPromiseオブジェクトは、引数で与えられたすべてのPromiseが解決した場合に解決し、
その結果は、引数で与えたPromise配列と同じ数の配列であり、各要素が解決した値となります。
(ここではすべてのPromiseの解決が成功する前提で書いています)
上のコードの例では、個々のAPI呼び出しが返すPromiseオブジェクトを、個別に awaitせず、配列に格納して Promise.all
に与えています。
これによって、すべてのAPIリクエストを一気に行って、結果が出るまで待つことになります。
複数非同期処理の並列実行で気をつけること
ここまでに書いたコードは、以下のように Array.map を使って置き換え可能です。 これが直感的であるかどうかは Promiseやasync/awaitに慣れているかどうかに依存すると思いますが、ここでワタシがよくやる間違いをひとつ書いておきます。
async function sample() { const apis = [ "http://path.to/rest/api1", "http://path.to/rest/api2", ]; const responses = await Promise.all(apis.map(api => fetch(api)); }
例えば、当初は非同期処理でなかったので Array.forEach
で書いていたのだが、途中で非同期処理に変更されたような時に、Array.forEach
を Array.map
に置き換えることを忘れていたり、
Promise.all
を await
するのを忘れていたり。これが実行時にエラーにならないんですよねえ。
できれば Array.forEach に AsyncFunction を与えた場合や、 Array.map の戻り値に対してなにも行っていない場合は警告を出してほしい。
AsciiDocのテーブルを入れ子にするにはビックリマーク
photo credit: Trey Ratcliff Boston Library via photopin (license)
AsciiDocのテーブル(表)のセル内に、これまたAsciiDocのテーブルを記述する方法です。
ごくたま~に、これをやりたくなるのですが、いつも書き方を忘れておりまして、 検索しても、なぜか上位のサイトで書き方が説明されていません。
「どこかの英文サイトの深~いところで確か見つけた記憶があるんだけどなあ~」と時間をかけて、やっと見つけて「あぁこれこれよ」という毎度の始末。
ということで、AsciiDocでネストしたテーブルを書く方法を、未来の自分のために書いておきます。時間がもったいないですからね。書いて覚えるスタイルで。
技術者のためのテクニカルライティング入門講座
髙橋 慈子(著)
新品 ¥2,178 27個の評価
Amazon.co.jpで詳細を見る
- 素のテーブル内にAsciiDocは書けません
- テーブル内にAsciiDocを記述する
- セルの中では「| (パイプ or 縦棒)」は使えない
- ネストされたテーブルでは「!」を使う
- それなら3段重ねはどうするの?
以降、このページ内の AsciiDocの変換は、npm miceroux を使っています。 使い方などは以下のページで説明しています。 takamints.hatenablog.jp
素のテーブル内にAsciiDocは書けません
そもそも AsciiDocの素のテーブル内に、AsciiDocを書いても、AsciiDocとして解釈されません。
以下のように、書いた内容がそのまま表示されてしまいます。
[cols="2,2,5"] |=== |Firefox |ブラウザ |FirefoxはオープンソースのWEBブラウザです。 下記のような特徴があります。: * 標準仕様準拠 * 高パフォーマンス * 高い可搬性 http://getfirefox.com[Firefoxをダウンロードする]! |===
ちょっとわかりにくいですが、箇条書きが箇条書きになっていませんね。
リンクが正しく解釈されているのはよくわかりません。
テーブル内にAsciiDocを記述する
セルの中のAsciiDocが正しく表示されるようにするには、セルの属性に a
を設定する必要があります。おそらく asciidoc
の意味ですよね。よく知りませんが。
AsciiDocのテーブルは(当たり前ですが)AsciiDocの文書ですから、この設定をしておかないとテーブルは書けません。
このことは Asciidoctor 文法クイックリファレンス(日本語訳) にしっかり書かれておりまして、文書も丸パクリしています。ちなみにワタシのメインブラウザはChromeです。
以下の例では、AsciiDocを記述する列全体に a
属性を設定しています。
[cols="2,2,5a"] |=== |Firefox |ブラウザ |FirefoxはオープンソースのWEBブラウザです。 下記のような特徴があります。: * 標準仕様準拠 * 高パフォーマンス * 高い可搬性 http://getfirefox.com[Firefoxをダウンロードする]! |===
以下のように、列単位ではなくセル単体に a
属性を設定することも可能です。
a|FirefoxはオープンソースのWEBブラウザです。
セルの中では「| (パイプ or 縦棒)」は使えない
AsciiDocのテーブルは |
(パイプ or 縦棒) を使用して記述します。
テーブル自体が |===
の行で始まって |===
の行で終わります。
また、その中のセルの区切りにも |
を使用します。
しかしセルの中にネストしたテーブルを書くために、これらの記号は使えません。 |===
が出てきたところで、外側のテーブルが終了してしまって意図通りの表示にはなりません。
ネストされたテーブルでは「!」を使う
じゃあ、どうするか?
「ネストしたテーブルでは、|
の代わりに !
(エクスクラメーション or ビックリマーク)を使います」ってことでした。
[cols="2,2,5a"] |=== |Firefox |ブラウザ |FirefoxはオープンソースのWEBブラウザです。 下記のような特徴があります。: * 標準仕様準拠 * 高パフォーマンス * 高い可搬性 http://getfirefox.com[Firefoxをダウンロードする]! [cols="2,2,5a"] !=== !Firefox !ブラウザ !FirefoxはオープンソースのWEBブラウザです。 下記のような特徴があります。: * 標準仕様準拠 * 高パフォーマンス * 高い可搬性 http://getfirefox.com[Firefoxをダウンロードする] !=== |===
それなら3段重ねはどうするの?
「じゃあ、3段階のネストはどうする?」って言うと、、、ワタシそれは知りません(あいすみません)。 できるのかどうかもわからないです。
ま、「そんなテーブルは見にくいだろうし、書かないでしょ?」ってことかもしれませんね。