Subterranean Flower

[翻訳]Dartの非同期サポート:フェーズ1

Author
古都こと
ふよんとえんよぷよぐやま!!!

この記事はDart Language Asynchrony Support: Phase 1の翻訳記事である。

Dartの非同期サポート:フェーズ1

非同期プログラミングをサポートする、新たな言語機能がDartにやってきます。この機能は、段階的に利用可能になります。この記事では、最も基本的な追加機能であるawait式およびasyncメソッドについて説明します。これらは非同期に関する機能の中で、最もよく使用されるものです。

自己責任

この記事で説明されている機能は、現在開発中のものです。必ずしもすべての要素が仕様に合致するわけではありません。アーリーアダプターの方がこれらの機能を利用するためには、dart:asyncをインポートする必要があるでしょう。最終的にはFutureはdart:coreへと移され、すべての場所で利用可能になります。

全体として、この記事はアーリーアダプターのためのお知らせだと考えてください。Dartにおける既存の非同期プログラミング手法について、ある程度理解しておいてください。

どの機能がサポート済みで、どのようにして有効化するかについての最新情報は、以下のissueをご覧ください:Dart VM, dart2js, WebStorm, Dart Editor

async関数

async関数は、本体にasync修飾子をつけられた関数です。

foo() async => 42;

async関数を呼び出すと、まずはFutureを返します。関数の本体は、後ほど実行されるようになります。関数本体の実行が終わると、関数呼び出しによって返されたFutureは、実行結果とともに完了します。関数本体が成功したか例外を生じたかにかかわらずです。この簡単な例では、foo()を呼び出すとFutureが結果として返されます。そのFutureは、最終的に42という数値として完了します。

async修飾子を用いずとも、同様の関数を書くことはできます。

foo() => new Future(() => 42);

async修飾子を使えば、定型句を使わずに済みます。しかし一番重要な点は、async修飾子を使うことで、これから紹介するawait式を関数内で使用可能になることです。後ほど、より完全にasync関数を理解するために、再び戻ってきます。

await式

await式を使うと、非同期のコードをまるで同期的なコードのように書くことができます。ファイルを表す変数myFileを考えてみましょう。(詳しくはdart:ioFileクラスをご覧ください。)ファイルをコピーするために、新しい場所newPathを以下のように宣言します。

String newPath = '/some/where/out/there';

以下がtrueになると期待するでしょう。

myFile.copy(newPath).path == newPath;

残念なことに、これはうまく動きません。DartのI/O APIは非同期であるため、コピー操作はFutureを返します。そして、そのpathを呼ぶことはできません。copy()から返されたFutureに、コールバックを登録しなければなりません。そしてそのコールバックは入力パラメータfとの比較を行います。

myFile.copy(newPath).then((f) => f.path == newPath);

これは少々うんざりします。しかし、込み入ったコードになればなるほど悪い方向へ向かいます。本当にやりたいことは、非同期ファイルコピーの完了を待って、結果を受け取って、処理を再開することです。await式を使えば、以下のように、まさにこの通りのことができます。

(await myFile.copy(newPath)).path == newPath;

await式を実行すると、myFile.copy()が実行され、Futureが生成されます。そして実行は中断され、Futureの処理が完了するまで待機します。Futureがファイルについての処理を完了したあと、実行が再開されます。このawait式の値はFutureが完了したときの値、つまり待ち望んでいたファイルになります。これでパスを取り出してnewPathと比較することが可能になりました。

一般にはawait式は以下の形式で表されます。

await e

ここで、eは単項式です。一般的に、eは非同期処理で、Futureとして評価されると期待されるものです。await式はeを評価し、現在の実行関数を、結果が準備完了になるまで中断します。つまり、Futureが完了するまでです。await式の結果はFutureの結果となります。


 

Note

一時停止の後、実行は後のイベントループサイクルで再開します。Dartのイベントループの解説については、The Event Loop and Dartをご覧ください。


もしFutureが値ではなくエラーで完了すれば、await式は実行再開時に同じエラーをスローします。これによって、非同期コードにおける例外ハンドリングが、非常に容易になります。

eがFutureとして評価されないときには?いえ、awaitはそれでも待機します(技術的には、結果をFutureでラップして、イベントループサイクルで完了するのを待ちます)。これが、Dartと他の言語の似たような機能との、ひとつの違いです。Dartでは、awaitは常に待機します。これにより、挙動がより予測しやすくなります。特に、条件のないawaitを内部に含むループがある場合、繰り返しごとに必ず中断するということが確信できます。

最後の重大な点として、await式はasync関数の中でしか使用できません。awaitを通常の関数の中で使用しようとすると、コンパイルエラーになります。通常の関数を中断しようとしても、それ以上同期的にはなりません。

async関数:詳細

await式がどのように動作するか理解したので、async関数を再訪して重要な細かい点について明らかにしていきましょう。

初めに、修飾子は関数のシグニチャと本体の間に書くことに注意してください。foo()は以下のようにも書くことができます。

foo() async { return 42; }

つまり、修飾子は=>か、関数本体の開きカーリーブレイスの前に来るということです。

修飾子はシグニチャの一部ではありません。単に関数実装の詳細であるというだけです。呼び出し側としては、async関数を呼び出すことは、通常の関数を呼び出すこととなんら変わりはありません。

同様の理由で、async修飾子は宣言された関数の戻り値の型にも影響を与えません。しかし実際に返すオブジェクトの型を変更します。return文が整数を返していても、呼び出し側にはFutureを返しています!async関数の内部では、return文は通常の関数とは違うように動作します。async関数内では、returnは呼び出し側へ返したFutureを完了させます。Futureはreturnされた式で完了します。

同様に、async関数内部で例外をスロー(または再スロー)すると、スローされたオブジェクトはFutureをエラーで完了させます。

もしreturnされた式が型Tであるとき、関数はFuture<T>(もしくはそのスーパータイプ)を返します。そうでなければ、静的警告が発生します。この例では戻り値の型を宣言していないので、戻り値はdynamicになっています。したがって警告は発生しません。

return文内の式がFuture<T>である場合、関数の戻り値はFuture<Future<T>>ではなく、Future<T>のままになります。別のFutureとして完了するFutureに対してできることは、より長く待機することを除いて、そうありません。なので、多重のFutureはasyncライブラリによって除去されます。タイプ規律はこの事実を認識できるように作られています。

最後に、Dartのasync関数は常に非同期的であることに気をつけてください。他の言語の、場合によっては完全に同期的になりうるasync関数とは異なります。Dartでは、async関数の呼び出しが呼び出し側に帰った後で、関数の全体が実行されます。

まとめ

ここに、今まで学んだことを取り入れた例があります。フレームごとにディスプレイを更新する、シンプルなアニメーションを実行することを考えましょう。

asyncawaitを使わずに書くと、以下のようなコードになるはずです。

import "dart:html"

main() {
  var context = querySelector("canvas").context2D;
  var running = true;    // falseにすると停止する。

  tick(time) {
    context.clearRect(0, 0, 500, 500);
    context.fillRect(time % 450, 20, 50, 50);

    if (running) window.animationFrame.then(tick);
  }

  window.animationFrame.then(tick);
}

これはそう複雑ではありませんが、まったくシンプルというわけでもありません。このコードはフレームを生成します。フレームが終了すると、コールバック関数tick()を実行するはずです。tick()は(アニメーションが停止されていない限り)次のフレームを生成して、自信をコールバック関数として再帰的に渡して処理を永続化します。関数tick()は処理の継続を表しています。そして、私達は継続がどれほど直感的で簡単なものか知っています。

言語の新しい機能を使えば、かわりに以下のように書くことができます。

import "dart:html";

main() async {
  var context = querySelector("canvas").context2D;
  var running = true;    // falseにするとゲームを停止する。

  while (running) {
    var time = await window.animationFrame;
    context.clearRect(0, 0, 500, 500);
    context.fillRect(time % 450, 20, 50, 50);
  }
}

このコードは自己説明的です。アニメーションが実行されている間、フレームを処理します。どちらを選ぶかはあなた次第です。どちらを選んでも容易に理解できるでしょう。