目次
- 1. はじめに
- 2. Promiseの概念を理解する
- 3. Promiseのからくりを理解する
- 4.Promiseとエラー処理
- 5. Promiseの結合
- 6. Promiseの実用的な理解
- 7. Promiseとは相性が悪いケースとは?
- 8. まとめ
- 参考文献
- 追加資料
- 資料とライブラリ
1. はじめに
JavaScriptの多くの実装は、シングルスレッドで動作するようになっています。そしてその言語の意味ゆえ、並行処理の命令にコールバックを使いがちです。JavaScriptで継続渡しスタイル(CPS)を使うのは特に間違いではありませんが、読みづらいコードになりやすく、必要以上に手順が増えるのも事実です。
これに関して多くの解決法が提案されており、Promiseを利用してこれらの並行処理を同期するという方法もそのひとつです。この投稿では、Promiseとは何か、どのように動作するのか、使うべき状況、使うべきでない状況とあわせて見ていきます。
注
この記事は、少なくとも高階関数、クロージャ、コールバック(継続渡しスタイル)を理解している読者を対象にしています。以上の知識なしで読んでも得るところはあるかもしれませんが、まずそれらの概念の基礎を理解しているほうがよいでしょう。
2. Promiseの概念を理解する
最初に、重要な問いに答えましょう。「ところで、Promiseって何?」
この問いの答えを得るため、現実の世界で、非常に一般的なシナリオを考えてみます。
幕間:行列が嫌いな女の子
とても人気のあるレストランで食事をしようしている女の子たち
Alissa P. Hackerとその女友達は、とても人気のあるレストランで食事をすることにしました。あいにく、予想されたとおり、レストランに到着するとテーブルは全ていっぱいでした。他の店であれば、彼女たちはあきらめるか、別の場所で食事をするか、テーブルが開くまで長い列に並ぶかを選ぶことになったでしょう。しかしAlissaは行列が嫌いでした。そしてこのレストランには彼女にとって完璧な解決法があったのです。
「これは、あなたの未来のテーブルを表す魔法の装置です…」
「心配しないで、お客様。この装置があなたのために全て手配します」と、小さな箱を持ったレストランの女性が言いました。
「これは何?」Alissaの友人、Rue Baeが尋ねます。
「これは、レストラン内のあなたの未来のテーブルを表す魔法の装置です…」。女性が答え、Baeを手招きしました。「本当は魔法ではないのですが、これはテーブルの準備が整い、あなた方が食事ができるようになったら教えてくれるのです」。
レストラン内にある未来のテーブルを表す魔法の装置
2.1 Promiseとは何か?
あなたの未来のテーブルのために代わりに並んでくれる”魔法の”装置のように、Promiseは将来使えるようになる何かを表すために存在します。プログラミング言語では、それは値です。
リンゴをまるごと入れると、スライスが出てくる。
同期処理の世界では、関数の計算は非常にシンプルに理解できます。何かを関数に入れると、関数は何かを返します。
この入力すると出力されるモデルはとても分かりやすく、多くのプログラマにとって最も身近なものです。JavaScriptの全ての構文構造と組み込み関数は、あなたの書く関数がこのモデルに沿うことを仮定しています。
しかし、このモデルには1つ大きな問題があります。何かを関数に入れることで美味しい何かを得るためには、そこに座って関数が仕事を終えるのを待つ必要があるのです。しかし理想としては、座って待つよりも、自分の時間でできるだけたくさんのことをしたいと誰もが思うでしょう。
この問題を解決するべく、Promiseは、欲しい値を待つ代わりに、その値の代替となるものを即座に得る方法を提案します。そうすれば私たちは自分たちの生活を続け、後のどこかの時点で、欲しい値を取りに戻って来るということができます。
Promiseは最終的に得られる値の代理品
リンゴをまるごと入れると、美味しいリンゴのスライスを後で入手するためのチケットが出てくる。
幕間:実行順序
では、Promiseとは何かを理解したところで、Promiseによって並行処理プログラムをより簡単に書く方法を見ていきます。その前に、1歩戻って、より根本的な問題、プログラムの実行順序について考えましょう。
JavaScriptのプログラマとして、プログラムが極めて独特な順序で実行されることに気づいた人もいるかもしれません。それはプログラムのソースコード内に書かれた命令の順序なのです。
- var circleArea = 10 * 10 * Math.PI;
- var squareArea = 20 * 20;
このプログラムを実行すると、JavaScript仮想マシンはcircleArea
の計算を行い、それが済むと、squareArea
の計算を行います。言い換えれば、プログラムはマシンに「これをやりなさい。そしてあれをやりなさい。それからあれをやりなさい。それから…」と説明しているのです。
ここでクイズの時間です。
なぜマシンはsquareArea
より先にcircleArea
を実行しなければならないのでしょうか。順序を逆にしたり、2つを同時に実行したりすると何か不都合があるのでしょうか。
結局、全てを順番に実行すると非常にハイコストなのです。もし、circleArea
の実行に長い時間がかかるなら、それが終わるまでsquareArea
は全く実行されません。実際、この例でも、どのような順序にするかは問題ではありません。結果は常に同じなのです。プログラムに表現されている順序は非常に恣意的です。
[…] 順序は非常にハイコストなのです。
私たちのコンピュータには、より多くのことをより速く行わせたいのです。そのためにまず、実行順序を完全に省きましょう。つまり、プログラムの中で、全ての式がきっかり同時に実行されるようにするのです。
このアイデアは先の例ではうまく動作します。しかし少しでも異なることを試すと破綻します。
- var radius = 10;
- var circleArea = radius * radius * Math.PI;
- var squareArea = 20 * 20;
- print(circleArea);
順序を全く持たない場合、他の式から得られる値を利用するにはどうすればいいのでしょう。まあ、それは不可能です。なぜなら、値が必要な時間までに計算されるという保証はないからです。
別の方法を試しましょう。プログラム内の唯一の順序は式の構成要素の間にある従属性によって定義されます。それは原則として、式は、他のことが既に実行されていたとしても、その全ての要素の準備ができ次第、実行されるということです。
単純な例の従属性グラフ
実行の際にプログラムがどのような順序で実行されるべきかを宣言する代わりに、各計算が互いにどのように関わり合っているかだけを定義しました。そのデータをもとに、コンピュータは上記の従属性グラフを作成し、最も効率的なプログラム実行方法を自ら把握します。
面白い事実
上記グラフはプログラミング言語Haskellにおいてプログラムが評価されるさまを如実に記述しています。しかし、同時に、Excelのようなより有名なシステムが式を評価する方法にも非常に似ているのです。
2.2 Promiseと並行処理
前の章で説明した、順序を式の間の従属性で明快に定義する実行モデルは力強く効率的ですが、JavaScriptに適用するにはどうしたらよいのでしょうか。
このモデルを直接JavaScriptに適用することはできません。言語の意味が本質的に同期式であり、かつ先頭から順に動くものだからです。しかし、別のメカニズムを作って式の間の従属性を記述し、これらの従属性を解決して、独自のルールに沿ってプログラムを実行させることはできます。1つの方法は、この従属性の概念をPromiseの中核に導入することです。
そこでPromiseの意味を改めて考えると、2つの大きな要素で成り立つことになります。1つは「値の代理品を作り、その代理品の中に値を入れることができる」ということ。それから、「1つの式と1つの値の間に従属性を作り、その式の結果を出すための新しいPromiseを作る」ということです。
未来の値の代理品を作成
値と式の間の従属性を作成
ここにおけるPromiseは、まだ計算していない値を表します。この代理品は不明瞭で、値は見えず、値と直接対話することもできません。さらに、JavaScriptのPromiseでは代理品から値を抜き出すこともできません。一度JavaScriptのPromiseに何かを入れると、Promiseから取り出すことはできないのです1。
このことはさほど有用ではありません。なぜなら何とかしてこれらの値を使う必要があるからです。そして代理品から値を抜き出せないとすると、それらを使うために別の方法を理解しなければなりません。結局、”抜き出しの問題”を解決する最も単純な方法は、プログラムにどう動作させたいのかを記述し、明示的に従属性を与え、実行のためにその従属性グラフを解決することです。
そのためには、実際の値を式の中に差し込み、この式の実行を確かに必要な時まで延期させる術が求められます。幸いJavaScriptには、第一級関数がまさにこの目的のために存在しています。
幕間:式の抽象化
a + 1
の形の式がある場合、この式で、一度準備ができればa
が差し込まれ得る値になる、といった抽象化を行うことが可能です。こうして、次の式が、
- var a = 2;
- a + 1;
- // { `a`を現在の値に置き換えると }
- // => 2 + 1
- // { 従って }
- // => 3
下のようなラムダ抽象化になります2。
- var abstraction = function(a) {
- return a + 1;
- };
- // `a`に値を差し込めます
- abstraction(2);
- // => (a => a + 1)(2)
- // { `a`を与えられた値に置き換えると }
- // => (2 => 2 + 1)
- // { 従って }
- // => 2 + 1
- // { 従って }
- // => 3
第一級関数は非常に強力な概念です(それらが匿名のラムダ抽象化であってもなくても)。第一級関数を持つJavaScriptは、後で値を差し込めるようPromiseの値を使った式を第一級関数に変換することによって、これらの従属性をとても自然に書くことができます。
3. Promiseのからくりを理解する
3.1. Promiseで式を順序付けする
Promiseの概念の性質を見直したので、次はマシンの内部でどのように動くかを理解しましょう。これからPromiseを生成するのに使う演算を記述し、値を置き、式と値の間の従属性を書きます。サンプルのため、偶然、既存のPromise実行には使われない、次のような非常に説明的な演算を使います。
-
createPromise()
は値の代理品を生成します。値は後の時点で提示されます。 -
fulfil(Promise, value)
はPromiseの中に値を置き、値に依存する式が計算されるようにします。 -
depend(Promise, expression)
は、式とPromiseの値の間の従属性を定義します。式の結果のための新しいPromiseを返し、新しい式がその値に依存できるようにします。
circlesとsquaresの例に戻りましょう。ここでは、より単純な式で始めます。Promiseを使い、同期のsquareArea
をプログラムの並行記述に変えます。squareArea
は、side
値にのみ依存しているので、よりシンプルなのです。
- // 元の式:
- var side = 10;
- var squareArea = side * side;
- print(squareArea);
- // 上記の式がこうなる
- var squareAreaAbstraction = function(side) {
- var result = createPromise();
- fulfil(result, side * side);
- return result;
- };
- var printAbstraction = function(squareArea) {
- var result = createPromise();
- fulfil(result, print(squareArea));
- return result;
- }
- var sidePromise = createPromise();
- var squareAreaPromise = depend(sidePromise, squareAreaAbstraction);
- var printPromise = depend(squareAreaPromise, printAbstraction);
- fulfil(sidePromise, 10);
コードの同期処理バージョンと比較すると、非常にノイズが多いのですが、この新バージョンはJavaScriptの実行順序と結びついていません。代わりに、その実行に働く唯一の制約は、私たちが記述した従属性です。
3.2. 最小限のPromise実行
もう1つ、未解決の問題があります。記述した従属性から得た順序に沿うように実際にコードを走らせるにはどうすればよいのでしょうか。JavaScriptの実行順序に従わないのであれば、何か別のものが望みどおりの実行順序を提供しなければなりません。
幸運にも、これまで使ってきた関数で簡単に定義できます。最初に値の表現方法と従属性を決めます。最も一般的なやり方は、このデータをcreatePromise
から返される値に追加することです。
まず、何かのPromiseはその値を代理できるものですが、必ずしも常に値が確定しているわけではありません。値は、fulfil
を呼び出して初めてPromiseに配置されます。最小限の表現は次の通りです。
- data Promise of something = {
- value :: something | null
- }
Promise of something
は、値null
を持った状態で生成されます。後の時点で、誰かがそのPromiseのためにfulfil
関数を呼び出すと、その時点からPromiseはfulfilされた値を持つでしょう。Promiseがfulfilされるのは一度だけなので、その値は残りのプログラムの間、Promiseが持ち続ける値になります。
value
(null
は有効な値です)を見るだけでは、Promiseが既にfulfilされたかどうかの把握が不可能な場合があります。それを2度fulfilしてしまうリスクを避けるため、Promiseの値が入っているか否かの状態を追跡する必要があります。そのためには前の表現を若干変えます。
- data Promise of something = {
- value :: something | null,
- state :: "pending" | "fulfilled"
- }
また、depend
関数によって作られた従属性も制御する必要があります。従属性が最終的に満たされることで、Promise内の値が満たされ、評価ができるようになりあます。1つのPromiseはその値に依存する多くの関数を持つことができるので、このための最小限の表現は次のようになります。
- data Promise of something = {
- value :: something | null,
- state :: "pending" | "fulfilled",
- dependencies :: [something -> Promise of something_else]
- }
Promiseのための表現を決定したので、新たなPromiseを作る関数の定義から始めましょう。
- function createPromise() {
- return {
- // promiseは最初は値を持たず、
- value: null,
- // また最初は"pending"状態です。後でfulfilledになります
- state: "pending",
- // そして、まだ依存関係はありません
- dependencies: []
- };
- }
シンプルな表現に決めたので、その表現のための新規オブジェクトの構築はかなり単純です。もう少し複雑な手順、Promiseへの従属性付与に進みましょう。
この問題の解決法の1つは、Promiseのdependencies
フィールドの中に作られた全ての従属性を引き出し、Promiseを、必要に応じて計算を行うインタープリタに送り込むやり方です。これを実行すると、インタープリタが開始しない限り従属は動作しません。今回はこの方法によるPromiseの実装は行いません。なぜなら多くの人がとるJavaScriptプログラムの一般的な記述方法に合わないからです3。
もう1つの解決法は、「Promiseがpending
状態にある間、本当に必要なのはPromiseの従属性を追跡することだけだ」という認識があれば分かります。なぜなら、Promiseがいったん完了すれば、関数を即座に実行できるからです。
- function depend(promise, expression) {
- // 式を計算できるようになった時に
- // その値を含むpromiseを返す必要があります
- var result = createPromise();
- // もしまだ式を実行できない場合、将来の値のために
- // 従属性リストに入れておきます
- if (promise.state === "pending") {
- promise.dependencies.push(function(value) {
- // 式の最終的な値に関心を持つため
- // この値をこのpromiseのresult promiseに入れます
- depend(expression(value), function(newValue) {
- fulfil(result, newValue);
- // `depend`はpromiseを一つ必要とするので、空のpromiseを返します
- return createPromise();
- })
- });
- // 式が実行可能な場合、ただ実行して、
- // 挿入すべき値が用意できます!
- } else {
- depend(expression(promise.value), function(newValue) {
- fulfil(result, newValue);
- // `depend`はpromiseを一つ必要とするので、空のpromiseを返します
- return createPromise();
- })
- }
- return result;
- }
depend
関数は、値が出るまで待機してから、従属の表現の実行を扱います。しかし、従属性を付与するのが早過ぎると、その関数はPromiseオブジェクトの配列で終了してしまい、仕事は終わらないということになります。実行の2番目の要素では、値を取得した時点で従属性を実行させる必要があります。幸運にも、fulfil
関数が使えます。
Promiseに入れたい値と共にfulfil
関数を呼ぶことによって、pending
状態にあるPromiseをfulfilできます。これはPromiseの値が使えるようになる前に作られた全ての従属性を呼び出し、その他の実行を手配する好機です。
- function fulfil(promise, value) {
- if (promise.state !== "pending") {
- throw new Error("Trying to fulfil an already fulfilled promise!");
- } else {
- promise.state = "fulfilled";
- promise.value = value;
- // ある従属関係がこのpromise他の従属関係を追加している可能性があるので、
- // 従属関係リストを一旦クリーンにしてコピーすることで
- // このイテレーションが影響を受けないようにします
- var dependencies = promise.dependencies;
- promise.dependencies = [];
- dependencies.forEach(function(expression) {
- expression(value);
- });
- }
- }
4.Promiseとエラー処理
幕間:計算失敗の場合
全ての計算がいつも有効な値を生成するとは限りません。a / b
や、a[0]
といったいくつかの関数は部分的関数であり、従ってa
やb
の値が、それぞれある範囲にあるときのみ定義可能になります。部分的な関数を含むプログラムを書いて、関数で扱えない状況に当たった時は、プログラムの実行を続けられません。言い換えれば、プログラム全体がクラッシュするということです。
プログラムに部分的な関数を組み込むより良い方法は、全域関数にすることです。つまり、先に定義されていなかった関数の部分を定義する方法です。一般的にこれは、関数が”成功”の形を扱える状況と、”失敗”の形を扱えない場合を考慮するということを意味します。この施策だけで、たとえ正しい値を生成できない計算に直面しても実行し続けるプログラムにすることができます。
部分関数の分岐
失敗の可能性のある部分それぞれで分岐させるのは合理的な処理方法ですが、必ずしも実用的ではありません。例えば、失敗し得る3つの計算を合成した場合、そのシンプルな合成に対して、少なくとも6種類の分岐を定義しなければならないのです。
各部分関数において分岐
面白い事実
プログラミング言語の中には、OCamlのように上述のスタイルのエラー処理を好むものがあります。起こっていることを明示するから、というのが理由です。一般に、関数型プログラミングは明示性を支持しますが、Haskellを含むいくつかの関数型言語は、エラー(やその他の事項)をより手際良く扱うため、Monad4と呼ぶインターフェースを使います。
理想としては、各部分式でエラーに対応するよりも、合成全体に対してただy / (x / (a / b))
とだけ書いて、可能性のあるエラーを一度で処理したいものです。プログラミング言語はそのために様々な方法を備えています。C言語やGoのように、完全にエラーを無視、あるいは少なくとも可能な限り触れるのを延期する言語もあれば、Erlangのように、プログラムをクラッシュさせつつ、復旧させるツールを供給する言語もあります。しかし最もよく見られるアプローチは、”エラー処理ハンドラ”を割り当て、失敗が起こりそうなコードを中断する方法です。JavaScriptはtry/catch
文を通じてそのアプローチを可能にします。下記が一例です。
手際良く失敗を処理するアプローチの例
4.1. エラーをPromiseで処理する
ここまでのPromiseの形は、失敗を認めません。そのため、Promiseで起こる全ての計算は常に有効な値の結果を生成しなければなりません。このことは、Promiseの中でa / b
などの計算を実行する時に問題になります。なぜなら、2 / 0
の場合のように、b
が0なら、その計算は正しい値を出せないからです。
新たなPromise構文の可能性
失敗の表現を極めて簡単に考慮するため、Promiseを修正します。現状、ここでのPromiseはpending
状態から始まっており、可能なのはfulfilだけです。新しい状態、rejected
を追加すれば、Promise内に部分関数を作ることができます。成功する計算はペンディングの状態で始まり、最終的にfulfilled
になります。失敗する計算はペンディングの状態で始まり、最終的にrejected
になります。
今や失敗の可能性があり、Promiseの値に依存する計算もそのことに気をつけなければなりません。とり急ぎ、depend
は、Promiseがfulfilledになった時に実行される式と、Promiseがrejectedになった時に実行される式を持つことになります。
よって、新しいPromiseの表現は次のようになります。
- data Promise of (value, error) = {
- value :: value | error | null,
- state :: "pending" | "fulfilled" | "rejected",
- dependencies :: [{
- fulfilled :: value -> Promise of new_value,
- rejected :: error -> Promise of new_error
- }]
- }
Promiseは適切な値あるいはエラーを含み、それがfulfilledかrejectedかが確定するまでnull
を含みます。この処理のため、従属性は適切な値とエラーの値に対して何を行うか知っていることも必要です。そこで従属性の配列を少しだけ変えます。
表現の変更の他、depend
関数も変えます。次の通りです。
- // 注意:ここで受け取る式が1つではなく2つになります
- function depend(promise, onSuccess, onFailure) {
- var result = createPromise();
- if (promise.state === "pending") {
- // ここで従属性は、以下の二つを得ます:
- // ・promiseが成功したときに何をすべきか
- // ・promiseが失敗したときに何をすべきか
- // これらの関数はおよそ以前のものと同じです
- promise.dependencies.push({
- fulfilled: function(value) {
- depend(onSuccess(value),
- function(newValue) {
- fulfil(result, newValue);
- return createPromise()
- },
- // 式を適用したときに起こるエラーについても
- // 注意する必要があります
- function(newError) {
- reject(result, newError);
- return createPromise();
- });
- },
- // rejectになった場合の分岐もfulfilled時の分岐と同様に
- // 行いますが、使う関数は
- // onFailure の式となります
- rejected: function(error) {
- depend(onFailure(error),
- function(newValue) {
- fulfil(result, newValue);
- return createPromise();
- },
- function(newError) {
- reject(result, newError);
- return createPromise();
- });
- }
- });
- }
- } else {
- // promisがfulfilledになったら、onSuccessを実行
- if (promise.state === "fulfilled") {
- depend(onSuccess(promise.value),
- function(newValue) {
- fulfil(result, newValue);
- return createPromise();
- },
- function(newError) {
- reject(result, newError);
- return createPromise();
- });
- } else if (promise.state === "rejected") {
- depend(onFailure(promise.value),
- function(newValue) {
- fulfil(result, newValue);
- return createPromise();
- },
- function(newError) {
- reject(result, newError);
- return createPromise();
- });
- }
- }
- return result;
- }
最後に、エラーをPromiseに置く手段が要ります。reject
関数を使いましょう。
- function reject(promise, error) {
- if (promise.state !== "pending") {
- throw new Error("Trying to reject a non-pending promise!");
- } else {
- promise.state = "rejected";
- promise.value = error;
- var dependencies = promise.dependencies;
- promise.dependencies = [];
- dependencies.forEach(function(pattern) {
- pattern.rejected(error);
- });
- }
- }
また、dependencies
フィールドを変更したため、fulfil
関数をレビューしなければなりません。
- function fulfil(promise, value) {
- if (promise.state !== "pending") {
- throw new Error("Trying to fulfil a non-pending promise!");
- } else {
- promise.state = "fulfilled";
- promise.value = value;
- var dependencies = promise.dependencies;
- promise.dependencies = [];
- dependencies.forEach(function(pattern) {
- pattern.fulfilled(value);
- });
- }
- }
これらの新規追加を行ったところで、Promise上に失敗し得る計算を配置できるようになりました。
- // 失敗可能性のある計算
- var div = function(a, b) {
- var result = createPromise();
- if (b === 0) {
- reject(result, new Error("Division By 0"));
- } else {
- fulfil(result, a / b);
- }
- return result;
- }
- var printFailure = function(error) {
- console.error(error);
- };
- var a = 1, b = 2, c = 0, d = 3;
- var xPromise = div(a, b);
- var yPromise = depend(xPromise,
- function(x) {
- return div(x, c)
- },
- printFailure);
- var zPromise = depend(yPromise,
- function(y) {
- return div(y, d)
- },
- printFailure);
4.2. Promiseの失敗の伝播
最後のコード例は決してzPromise
を実行しません。c
が0なので、計算div(x, c)
は失敗するからです。これはまさに予測していたことですが、今はPromiseの中に計算を定義するたびに失敗の分岐を渡す必要があります。理想としては、同期処理のtry/catch
で行ったのと同様に、必要な箇所にのみ失敗の分岐を定義したいところです。
Promiseにとってこの機能性のサポートはたやすいのです。制御フローにおいてよく起こることですが、抽象化不可の場合のたびに、成功と失敗の分岐を定義するだけです。例えば、JavaScriptでは、if
文やfor
文を抽象化することはできません。それらは第二級制御フローメカニズムであり、編集、送り出し、変数への格納ができないためです。このPromiseは第一級オブジェクトなので、失敗と成功の具体的な表現を持っています。それらは作成された時点だけでなく、いつでも必要なときに検査、対話ができます。
try/catch
と似たものを手に入れるため、成功と失敗の表現を使って2つのことを行わねばなりません。
-
失敗からの復旧: 計算が失敗した場合は、その値を筋の通る何らかの成功の形に変えます。例えば、
Map
やArray
構造体からデータを取り戻す時に、初期値を使ったりします。マップ上に"foo"
がない場合、map.get("foo").recover(1) + 2
は3を与えます。 -
いかなる時も失敗する: 計算が成功した場合は、その値を失敗に変え、失敗だった場合は、失敗を維持しなければなりません。前者は短絡演算子が使え、後者は失敗の伝播が使えます。この2つによって、部分式が失敗した場合、完全に失敗する
(a / b) / (c / d)
の意味を取得できます。
幸運にも、depend
関数がこの仕事のほとんどを既に行っています。depend
はその表現がPromise全体を返し、値だけでなく、Promiseが存在する状態の伝播も要求するからです。successful
分岐だけを定義してPromiseが失敗した場合、値だけでなく失敗の状態も伝播させたいので、これは重要です。
これらのメカニズムが既に適切だとして、シンプルな失敗の伝播のサポート、エラー処理と、失敗の時の短絡演算を行うには、2つの演算子の追加が必須です。chain
、これは、失敗の際の短絡演算子ではなく、Promiseの成功した値に関して従属性を生成します。そしてrecover
は、Promiseの失敗した値に関して従属性を生成し、そのエラーから復旧させます。
- function chain(promise, expression) {
- return depend(promise, expression,
- function(error) {
- // 等価なpromiseを作成することで
- // 状態と値を伝播する
- var result = createPromise();
- reject(result, error);
- return result;
- })
- }
- function recover(promise, expression) {
- return depend(promise,
- function(value) {
- // 等価なpromiseを作成することで
- // 状態と値を伝播する
- var result = createPromise();
- fulfil(result, value);
- return result;
- },
- expression)
- }
こうして、2つの関数を使って、先述の部分サンプルを単純化できます。
- var a = 1, b = 2, c = 0, d = 3;
- var xPromise = div(a, b);
- var yPromise = chain(xPromise, function(x) {
- return div(x, c)
- });
- var zPromise = chain(yPromise, function(y) {
- return div(y, d);
- });
- var resultPromise = recover(zPromise, printFailure);
5. Promiseの結合
5.1. Promiseを確定的に結合する
Promiseの演算子の配列が一連の従属性を要求する一方、Promiseの並行的な結合はただ、Promiseが相互に従属性を持たないことだけを求めます。
Circleの例における計算は、本来的に並行でした。radius
式とMath.PI
式は相互に依存しておらず、別々に計算されますが、circleArea
は両方に依存します。コードで表したのが下記です。
- var radius = 10;
- var circleArea = radius * radius * Math.PI;
- print(circleArea);
同じものをPromiseで表すと、次のようになります。
- var circleAreaAbstraction = function(radius, pi) {
- var result = createPromise();
- fulfil(result, radius * radius * pi);
- return result;
- };
- var printAbstraction = function(circleArea) {
- var result = createPromise();
- fulfil(result, print(circleArea));
- return result;
- };
- var radiusPromise = createPromise();
- var piPromise = createPromise();
- var circleAreaPromise = ???;
- var printPromise = chain(circleAreaPromise, printAbstraction);
- fulfil(radiusPromise, 10);
- fulfil(piPromise, Math.PI);
ここで小さな問題があります。circleAreaAbstraction
は2つの値に依存しますが、depend
にできるのは1つの値に依存する式の従属性を定義することだけなのです。
この制限の中で作業を進める方法はいくつかありますが、シンプルなものから始めます。もしdepend
が1つの式に対して1つの値を提供できるなら、クロージャの中で値を取得し、Promiseから1つずつ値を引き出すことができるはずです。これはいくつか暗示的な順序を生成しますが、並行処理への影響はそれほど大きくありません。
- function wait2(promiseA, promiseB, expression) {
- // まず、promiseAからの値を抜き出します
- return chain(promiseA, function(a) {
- // その後、promiseBからの値を抜き出します
- return chain(promiseB, function(b) {
- // 両方の値にアクセスできるようになったので
- // 2つ以上の値に依存するような
- // 式を実行できるようになります
- var result = createPromise();
- fulfil(result, expression(a, b));
- return result;
- })
- })
- }
これによって、circleAreaPromise
を下記のように定義できます。
- var circleAreaPromise = chain(wait2(radiusPromise, piPromise),
- circleAreaAbstraction);
3つの値に依存する式にwait3
を、4つの値に依存する式にwait4
などを定義できました。しかしwait*
は暗示的な順序を生成し(Promiseは特定の順序で実行されます)、差し込もうとしている値の数を事前に知っていることが条件です。そのため、例えばPromiseの配列全体を待ちたい場合には使えません(ただ、wait2
とArray.prototype.reduce
をそのために結合することはできます)。
別の解決法は、Promiseの配列を受け取り、それぞれを可能な限り早く実行し、元のPromiseが持っている値の配列のためにPromiseを返すことです。このアプローチは、シンプルな有限状態機械(FSM)を実行する必要があるために少々複雑ですが、暗示的な順序はありません(JavaScript独自の実行を除きます)。
- function waitAll(promises, expression) {
- // promiseの値の配列で、
- // それぞれ順繰りに値を入れて行きます
- var values = new Array(promises.length);
- // 待機中のpromiseの個数
- var pending = values.length;
- // 結果のpromise
- var result = createPromise();
- // promiseが解決済みか否か
- var resolved = false;
- // それぞれのpromiseを実行することから始めます。
- // 元の順序を保持しておくことで、結果の配列のどこに
- // 値を格納すればいいかわかるようにします
- promises.forEach(function(promise, index) {
- // それぞれのpromiseについて、解決を待ったのち
- // `values`配列にその値を入れます
- depend(promise, function(value) {
- if (!resolved) {
- values[index] = value;
- pending = pending - 1;
- // 全てのpromiseの待機が終了したら、
- // 値の配列を結果のpromiseに入れることができます
- if (pending === 0) {
- resolved = true;
- fulfil(result, values);
- }
- }
- // このpromiseについて、他に関心を持つべきことはないので、
- // `depend` が必要とする分の空のpromiseを返します
- return createPromise();
- }, function(error) {
- if (!resolved) {
- resolved = true;
- reject(result, error);
- }
- return createPromise();
- })
- });
- // 最後に、最終的な値の配列のためにpromiseを返す
- return result;
- }
waitAll
をcircleAreaAbstraction
のために使う場合、次のようになります。
- var circleAreaPromise = chain(waitAll([radiusPromise, piPromise]),
- function(xs) {
- return circleAreaAbstraction(xs[0], xs[1]);
- })
5.2. Promiseを非確定的に結合する
Promiseの結合の仕方を見てきましたが、ここまでは確定的に結合するしかありませんでした。例えばもし、2つの計算から速いほうを選びたい場合には役立ちません。2つのサーバ上で何かを探し、どちらか答えを返した方をとってもよいかもしれませんが、ここではとにかく最速の方法を使います。
この作業のサポートとして、いくつか非確定性を導入します。特に2つのPromiseがある場合、最速で解決する方のpromiseの値と状態を取る演算が必要です。演算の背景にあるアイデアはシンプルです。2つのPromiseを並行に走らせ、最初の解決を待ち、それを結果としてのPromiseに伝播するのです。状態を維持するため、実装はいくらか複雑になります。
- function race(left, right) {
- // 結果のpromiseの生成
- var result = createPromise();
- // 両方のpromiseを並行して待ちます。
- // doFulfil と doReject は、先に返ってきた
- // promiseの結果/状態を伝播します。
- // これは、`result`の現在の状態をチェックし
- // `pending`状態にあるかを調べることでおこないます
- depend(left, doFulfil, doReject);
- depend(right, doFulfil, doReject);
- // 結果のpromiseを返します
- return result;
- function doFulfil(value) {
- if (result.state === "pending") {
- fulfil(result, value);
- }
- }
- function doReject(value) {
- if (result.state === "pending") {
- reject(result, value);
- }
- }
- }
これによって、それらを非確定的に選んで演算の結合を開始できます。先のサンプルで見てみましょう。
- function searchA() {
- var result = createPromise();
- setTimeout(function() {
- fulfil(result, 10);
- }, 300);
- return result;
- }
- function searchB() {
- var result = createPromise();
- setTimeout(function() {
- fulfil(result, 30);
- }, 200);
- return result;
- }
- var valuePromise = race(searchA(), searchB());
- // => valuePromise は最終的に 30
2つ以上のPromiseからの選択が可能です。なぜならrace(a, b)
は、基本的に速く解決する方に従ってa
またはb
になるからです。race(c, race(a, b))
を持ち、b
が先に解決した場合、race(c, b)
と同様になるのです。もちろん、race(a, race(b, race(c, …)))
と入力するのは最善ではないので、シンプルなコンビネータを書きましょう。
- function raceAll(promises) {
- return promises.reduce(race, createPromise());
- }
そして実際に使ってみます。
- raceAll([searchA(), searchB(), waitAll([searchA(), searchB()])]);
2つのPromiseから非確定的に選ぶ別の方法は、1番目のfulfilが成功するのを待つことです。例えば、ミラーのリストから正しいダウンロードリンクを見つける時、初めの失敗だけのために失敗するよりは、一番に得られるミラーからダウンロードし、全てが失敗の場合に失敗としたいはずです。これを取得するため、attempt
演算子を書きましょう。
- function attempt(left, right) {
- // 結果のpromiseの生成
- var result = createPromise();
- // doFulfilは最初に成功したpromiseの
- // 状態/値を伝播し、doRejectは
- // 全てのpromiseが失敗するまでエラーを収集します
- //
- // 起こったエラーを収集する必要があります
- var errors = {}
- // `race`と同様、両方のpromiseを待機します。
- // `race`との違いは、`doReject`が
- // 「どちらのpromiseがrejectを返したのか」を知る必要がある点です。
- // エラーをトラックする必要があるからです。
- depend(left, doFulfil, doReject('left'));
- depend(right, doFulfil, doReject('right'));
- // 最後に、結果のpromiseを返す
- return result;
- function doFulfil(value) {
- if (result.state === "pending") {
- fulfil(result, state);
- }
- }
- function doReject(field) {
- return function(value) {
- if (result.state === "pending") {
- // もしまだpendingであれば、エラーの収集を注意深く続けます。
- // 取得したエラーが、エラーを収集するオブジェクトの
- // 正しいフィールドに格納されているか確認します
- errors[field] = value;
- // 全てのエラーを収集した場合、結果のpromiseをrejectします。
- // rejectの際には、起こったエラーを
- // 正しい順序で格納してrejectします
- if ('left' in errors && 'right' in errors) {
- reject(result, [errors.left, errors.right]);
- }
- }
- }
- }
- }
用法はrace
と同じなので、attempt(searchA(), searchB())
は、ただ最初のPromiseではなく、成功した最初のPromiseを返します。しかし、race
とは異なり、attempt
はエラーを集めるので、自然には集約できません。いくつかのPromiseをattemptしたい場合は更なる記述が必要です。。
- function attemptAll(promises) {
- // すべてのpromiseを収集するので、まずはrejectから始める
- // 必要があります。さもなければ、エラーがあった際にattemptが
- // 終了しません
- var initial = createPromise();
- reject(initial, []);
- // 最後に、promiseの結合のために`attempt`を使います。
- // 各ステップでエラーの配列をflattenすることに注意
- return promises.reduce(function(result, promise) {
- return recover(attempt(result, promise), function(errors) {
- return errors[0].concat([errors[1]]);
- });
- }, createPromise());
- }
- attemptAll([searchA(), searchB(), searchC(), searchD()]);
6. Promiseの実用的な理解
ECMAScript 2015はPromiseの概念をJavaScript用に定義しますが、これまで非常にシンプルな、しかし略式のPromiseの実装を見てきました。理由はECMAScript標準のPromiseは複雑過ぎ、基礎から概念を説明するのが難しいだろうと考えたからです。今はPromiseが何であるか、各アスペクトがどのように実装されるかを知っていますから、標準のPromiseに進むのは容易でしょう。
6.1. ECMAScript Promiseの導入
ECMAScript言語の新バージョンでは標準のPromiseをJavaScriptで定義します。この基準はいくつかの方法で導入してきた最小限のPromiseの実装とは異なり、より複雑ですが、より実用的で簡単に使えます。以下のテーブルは各実装の相違点をリストアップしています。
ここまで見てきたPromise | ES2015 Promise |
---|---|
p = createPromise() |
p = new Promise(...) |
fulfil(p, x) |
p = new Promise((fulfil, reject) => fulfil(x)) |
p = Promise.resolve(x) |
|
reject(p, x) |
p = new Promise((fulfil, reject) => reject(x)) |
p = Promise.reject(x) |
|
depend(p, f, g) |
p.then(f, g) |
chain(p, f) |
p.then(f) |
recover(p, g) |
p.catch(g) |
waitAll(ps) |
Promise.all(ps) |
raceAll(ps) |
Promise.race(ps) |
attemptAll(ps) |
(None) |
標準Promiseの主なメソッドは、Promiseオブジェクトを導入するnew Promise(…)
、それを変形させる.then(…)
です。その動作には、ここまでで書いた演算子と比較していくつかの違いがあります。
new Promise(f)
は新規Promiseオブジェクトを導入し、特定の値で成功か失敗をする計算を行います。成功か失敗の動作は、想定どおり、関数f
に渡された2つの関数引数によって取得されます。
- var p = createPromise();
- fulfil(p, 10);
- // Becomes:
- var p = new Promise((fulfil, reject) => fulfil(10));
- // ---
- // And:
- var q = createPromise();
- reject(q, 20);
- // Becomes:
- var p = new Promise((fulfil, reject) => reject(20));
promise.then(f, g)
は値のための穴を持つ式とPromise内の値の間に従属性を作る演算で、depend
演算に似ています。f
とg
共に、オプションの引数で、Promiseを供給されない場合、その状態で値を伝播します。
depend
とは異なり、.then
は複雑な演算で、Promiseの利用を簡単にしようとします。.then
に渡された関数実引数はPromiseか、一般の値を返します。その場合、これらは自動的にPromiseに置かれます。
- depend(promise, function(value) {
- var q = createPromise();
- fulfil(q, value + 1);
- return q;
- })
- // ---
- // Becomes:
- promise.then(value => value + 1);
これらは、先の記述と比べ、Promiseを使ったコードを簡潔に読みやすくします。
- var squareAreaAbstraction = function(side) {
- var result = createPromise();
- fulfil(result, side * side);
- return result;
- };
- var printAbstraction = function(squareArea) {
- var result = createPromise();
- fulfil(result, print(squareArea));
- return result;
- }
- var sidePromise = createPromise();
- var squareAreaPromise = depend(sidePromise, squareAreaAbstraction);
- var printPromise = depend(squareAreaPromise, printAbstraction);
- fulfil(sidePromise, 10);
- // ---
- // Becomes
- var sideP = Promise.resolve(10);
- var squareAreaP = sideP.then(side => side * side);
- squareAreaP.then(area => print(area));
- // Which is more akin to the synchronous version:
- var side = 10;
- var squareArea = side * side;
- print(squareArea);
複数の値に並行に従属させるのは、Promise.all
演算です。先に見たwaitAll
演算に似ています。
- var radius = 10;
- var pi = Math.PI;
- var circleArea = radius * radius * pi;
- print(circleArea);
- // ---
- // Becomes:
- var radiusP = Promise.resolve(10);
- var piP = Promise.resolve(Math.PI);
- var circleAreaP = Promise.all([radiusP, piP])
- .then(([radius, pi]) => radius * radius * pi);
- circleAreaP.then(circleArea => print(circleArea));
エラーと成功の伝播は.itself
演算そのものが扱います。そして.catch
演算は、成功の分岐を定義することなく.then
を呼び出す簡潔な方法です。
- var div = function(a, b) {
- var result = createPromise();
- if (b === 0) {
- reject(result, new Error("Division By 0"));
- } else {
- fulfil(result, a / b);
- }
- return result;
- }
- var a = 1, b = 2, c = 0, d = 3;
- var xPromise = div(a, b);
- var yPromise = chain(xPromise, function(x) {
- return div(x, c)
- });
- var zPromise = chain(yPromise, function(y) {
- return div(y, d);
- });
- var resultPromise = recover(zPromise, printFailure);
- // ---
- // Becomes:
- var div = function(a, b) {
- return new Promise((fulfil, reject) => {
- if (b === 0) reject(new Error("Division by 0"));
- else fulfil(a / b);
- })
- }
- var a = 1, b = 2, c = 0, d = 3;
- var xP = div(a, b);
- var yP = xP.then(x => div(x, c));
- var zP = yP.then(y => div(y, d));
- var resultP = zP.catch(printFailure);
6.2. .then
の分析
.then
メソッドはいくつかの点で先に書いたdepend
関数とは違っています。.then
は結果値と複数の計算の従属関係を定義する1つのメソッドですが、同時にメソッドの使用を大多数のケースで容易にしようとします。このことが.then
を複雑なメソッドにしていますが5、先に見たからくりとこの新しいメソッドを関連付ければ分かりやすいでしょう。
.then
は自動的にただの値を引き上げる
先に作ったdepend
関数はPromiseの領域で作動しました。従属の計算がPromiseそのものを返すために、Promiseを返すように作られています。.then
にはこの条件がありません。もし従属の計算が42
といったただの値を返した場合、.then
はそれを、値を含むPromiseに変換します。本来、.then
は、必要に応じてただの値をPromiseの領域に引き上げるものです。
単純化したタイプのdepend
関数と比較します。
- depend : (Promise of α, (α -> Promise of β)) -> Promise of β
単純化したタイプの.then
メソッドです。
- promise.then : (this: Promise of α, (α -> β)) -> Promise of β
- promise.then : (this: Promise of α, (α -> Promise of β)) -> Promise of β
唯一depend
関数でできることは、何かのPromiseを返すこと(それから、同じ何かを結果のPromiseに持たせること)ですが、.then
関数は簡便性のため、ただの値を(promiseでラップすることなく)返すことを許容しています。
.then
はネストのPromiseを使えなくする
ECMAScript 2015 Promiseが一般的なユースケースでの用法を簡素化しようとしているもう1つの方法は、Promiseのネストの禁止です。.then
メソッドを持つすべてを同化することによるもので、同化させたくない場合は問題になり得ます6。しかし、そうでなければ戻り値のタイプのマッチングを考えずに済むので便利です。
この特徴ゆえ、.then
メソッドに、非従属タイプに認識可能なタイプを与えることは不可能です。しかし、それは大まかに説明すると以下のような形になります。
- Promise.resolve(1).then(x => Promise.resolve(Promise.resolve(x + 1)))
は、下記と等価です。
- Promise.resolve(1).then(x => Promise.resolve(x + 1))
これは、Promise.resolve
でも強制されますが、Promise.reject
では実行されません。
.then
は例外を具象化する
.then
メソッドを通して付加した従属の計算を評価する間、同期的に例外が起こった場合、その例外はrejectされたPromiseとして見つけられ、具象化されます。これが本質的に意味するのは、「.then
メソッドを通してPromiseの値に付加した全ての計算は、暗示的にtry/catch
ブロックにラップされたかのように扱わねばならない」ということです。例えば下記のように。
- Promise.resolve(1).then(x => null());
上記は、次と等価です。
- Promise.resolve(1).then(x => {
- try {
- return null();
- } catch (error) {
- return Promise.reject(error);
- }
- });
Promiseのネイティブの実装はこれらを追跡し、処理がされていないものを報告します。Promiseにおいて”取得されたエラー”の構成に関する仕様はないので、エラーリポートのされかたは開発ツールごとに異なります。例えば、Chromeの開発ツールは、rejectされたPromiseのインスタンスの全てをコンソールに出力するため、誤検知を表示することがあります。
.then
は従属性を非同期的に呼び出す
先のPromiseの実装は従属する計算を同期的に呼び出しましたが、標準のECMAScript Promiseは非同期的にそれを行います。適切な手段、.then
メソッドを使わなければ、Promiseの値への依存は難しいということです。
従って、次のコードは動きません。
- var value;
- Promise.resolve(1).then(x => value = x);
- console.log(value);
- // => undefined
- // (`value = x` happens here, after all other code has finished)
これによって従属計算は常に空のスタックで確実に実行されますが、そういった保証はECMAScript 2015では重視されていません。全ての実装が適切な末尾再帰をサポートすることが条件になっているからです7。
7. Promiseと相性が悪いケースとは?
Promiseは並行処理の基本形として良く機能しますが、継続渡しスタイルほど一般的ではなく、全てのケースにおいて最善の基本形でもありません。Promiseは最終的には計算される値のプレースホルダなので、これらの値そのものを使うコンテキストでのみ意味をなします。
promiseは値のコンテキストにおいてのみ意味をなす
Promiseをそれ以外の何にでも使おうとすると、メンテナンスが困難で、読みにくく、拡張もししづらい、非常に複雑なコードベースになってしまいます。以下はPromiseを使うべきでない例の一部です。
-
特定の値の計算の進行状況を通知すること。promiseは値そのものと同じコンテキストで使われるので、文字列そのものを与えられて特定の文字列計算の進捗状況を知ることができないのと同様、Promiseでもそれは不可能です。このため、どれだけのファイルがダウンロードされたかを知りたい場合は、Eventなどを別に用意するのをおすすめします。
-
時間をかけて複数の値を生成すること。Promiseは1つの結果となる値のみを表現できます。経時的に複数の値が生成され得る場合(非同期イテレータと同義)、Stream、ObservableまたはCSPチャネルなどを要するでしょう。
-
動作を表現すること。これも、Promiseは順番に実行できないことを意味します。いったんPromiseを使うと、そのために値を提供する計算も始まっているからです。動作には、継続渡しスタイル、継続モナド、またはC#と同様にTask (co)monadを使うことができます。
8. まとめ
Promiseは結果の値を扱う優れた方法です。非同期で計算される値に依存するプロセスの合成と同期ができるようになります。ECMAScript 2015標準のPromiseには、プロセスをクラッシュさせるエラーを自動的に具象化するなど、特有の課題がありますが、前述の問題を十分適切に扱えるツールです。実際に使うかどうかは別として、それらの特徴と動作を理解するのが大切です。今後は全てのECMAScriptプロジェクトにさらに普及していくと考えられるからです。
参考文献
ECMAScript® 2015 言語仕様
Allen WirfsがJavaScriptのPromiseの標準を定義。
鏡の国のAlice
Andreas Rossberg、Didier Le Botlan、Guido Tack、Thorsten Brunklaus、Gert Smolkaがプログラミング言語Aliceを紹介。FutureとPromiseで並行処理をサポート。
Haskell 98 言語とライブラリ
Simon Peyton Jonesが、プログラミング言語Haskellの意味論を分かりやすく説明。
Communicating Sequential Processes
C. A. R. Hoareが、確定的・非確定的の選択などのプロセスの並行コンビネーションを説明。
関数型言語のためのモナド
Philip Wadlerが、関数型言語においてモナドがどのようにエラーを扱うかなどを説明。Promiseのシーケンス処理とエラーの扱いはモナドの形式に非常に似ている。ただし、ECMAScript 2015標準ではPromiseはモナドインターフェースを実装しない。
追加資料
ブログポストのソースコード
このブログポストのコメント付きソースコード全て。最低限ECMAScript 2015 の仕様に準拠するPromiseの実装も含む。
有害なPromises/A+
Quildreen MottaがPromises/A+とECMAScript 2015 Promise標準が持つ問題を、複雑さ、エラーの扱い、パフォーマンスの面から議論する。
Frisby教授のこれだけで十分な関数型言語ガイド
Brian Lonsdorfによる、JavaScript関数型言語の導入本。
コールバックは命令型、Promiseは関数型:見逃されがちなノードの大きなチャンス
James Coglanが、プログラムの実行順序を記述するための継続渡しスタイルとPromiseを比較。
シンプルは簡単だ
直接Promiseに関係ないが、Rich Hickeyが語るデザインのコンテキストにおける「シンプル」「簡単」は、常にプログラミングにもあてはまる。
適切な末尾再帰のハーモニー
Dave HermanによるECMAScriptでProper Tail Callsを使うメリットの説明。
あなたのマウスはデータベース
Erik Meijerが、Observableの概念を用いてRxのEventベース、非同期の計算のコーディネートと管理について議論する。
ストリームハンドブック
James Hallidayによる、Streamを用いてNode.jsプログラムを書く基本。
ケーススタディ:JavaScriptの継続渡しスタイル
Matt Mightが、継続渡しスタイルを使い、JavaScriptのスレッドが中断されない計算を扱う方法を説明。
継続モナド
Gabriel Gonzalezが、モナドとしての継続の概念をHaskell プログラミング言語のコンテキストで議論する。
停止とプレイ:非同期のC#
Claudio Russoが、Task comonadを使ったC#の非同期計算、その解決法と他のメソッドとの関わりを説明。
資料とライブラリ
ES6-PROMISE
ECMAScript 2015標準のPromise、ES2015を実装していないプラットフォームのためのポリフィル。
脚注
1: Promises/A、Promises/A+とその他の一般的なPromise式では、Promiseの値を抜き出すことはできません。
Rhinoや Nashornなど一部のJavaScript環境では、値の抜き出しをサポートするPromiseの実装へのアクセスを持ち得る可能性はあります。JavaのFutureが一例です。
Promiseから未計算の値を抜き出すには、値が計算されるまでスレッドを中断しなければなりません。ほとんどのJS環境はシングルスレッドのため、それは機能しません。
2: 「ラムダ抽象化」はある式の中で抽象化を行う匿名の関数にラムダ計算が与える名称です。JavaScriptの匿名関数はLCのラムダ抽象化と同等ですが、JavaScriptではさらに自らの関数に名前をつけることもできます。
3: “計算の定義”と”計算の実行”の分離には、Heskellプログラミング言語が役立ちます。Haskellプログラムは、IO
データ構造体まで評価する巨大な式に過ぎません。この構造体はいくらかこの記事で定義したPromise
構造体に似ています。そこではプログラム内の計算間の従属性が定義されるだけです。
HaskellではプログラムはタイプIO
の値を必ず返し、分離されたインタープリタに渡されます。インタープリタはIO
計算を行い、それが定義した従属性を尊重することしか知りません。JSのために似たものを定義することは可能かもしれません。それを行うと、全てのJSプログラムは結果としてPromiseに至るただ1つの式になるでしょう。そのPromiseは、Promiseとその従属性の実行の仕方を認識している別の要素に渡されるはずです。
この形のPromsieの実行については、Pure Promisesのサンプルディレクトリを見てください。
4: モナドは、シーケンス処理に頻繁に使われるインターフェースです。次のような演算の構造体として書かれます。
- class Monad m where
- -- Puts a value in the monad
- of :: ∀a. a -> Monad a
- -- Transforms the value in the monad
- -- (The transformation must maintain the same type)
- chain :: ∀a, b. m a -> (a -> m b) -> m b
この形では、JavaScriptの「カンマ演算子」(print(1); print(2)
が一例)のようなものを、モナドchain
演算子print(1).chain(_ => print(2))
の使用として考えることが可能です。
5: これはRich Hickeyが言うところの”複雑”と”容易”です。.then
は確かに簡単なメソッドです。シンプルな概念で、一般的なユースケースに使えます。要は、.then
は多くの作業を行い、そこには大量の重複があるということです。
一方で、シンプルなAPIはこれらの分離した概念を、.then
メソッドと一緒に使える別の関数に移動させます。
6: .then
メソッドはPromiseに見える全てのものの状態と値をどうかします。歴史上、これはインターフェースチェックを通して行われます。オブジェクトが‘“.thenメソッドを供給したかどうかをチェックするだけで、それには、内包する
.thenメソッドがPromiseの
.then“`メソッドに準拠していない全てのオブジェクトが含まれます。
標準のPromiseが、既存のPromise実装との下位互換に制限されていなければ、より信頼のおけるテストができるでしょう。その際はインターフェースのSymbolや、それに類するものを使います。
7: Proper Tail Callsは末尾の呼び出しが一定のスタックで起こることを保証します。プログラムや計算が末尾再帰で占められており、スタックが膨らまない限り保証されるので、そのようなコードではスタックオーバーフローエラーが起こり得ません。ところで、Proper Tail Callsにより、言語は通常の関数呼び出しの負荷を処理する必要がないため、上のようなコードの速度は上がります。