async-await 20170208.350(in MELPA)
Async/Await

概要

async-await.el はAsync/Awaitの
Emacs Lispによる実装です。

Async/AwaitはC#由来で
TypeScriptにも導入されている
イケてる非同期処理です。

promise.el と併用することで、
非同期処理をさらに平易に書けるようになります。

Promiseのおかげで、
非同期処理を流れに沿って
書けるようになりましたが、

Async/Awaitを使うと、
もっとわかりやすくなります。

使い方は

  • Promiseを await で受け、
  • defunasync-defun に置き換える

だけです。

async-defun のlambda版である
async-lambda も用意されています。

async-await.elはEmacs 25.1の新機能
generator.el の貴重な使用例でもあります。

ジェネレータ・イテレータが
Emacs標準添付になったことは
ほとんど知られていません。

インストール

パッケージシステムを初めて使う人は
以下の設定を ~/.emacs.d/init.el の
先頭に加えてください。

(package-initialize)
(setq package-archives
      '(("gnu" . "http://elpa.gnu.org/packages/")
        ("melpa" . "http://melpa.org/packages/")
        ("org" . "http://orgmode.org/elpa/")))

初めてasync-awaitを使う方は
以下のコマンドを実行します。

M-x package-install async-await

アップグレードする方は、
以下のコマンドでアップグレードしてください。
そのためにはpackage-utilsパッケージが必要です。

M-x package-install package-utils (初めてアップグレードする場合のみ)
M-x package-utils-upgrade-by-name async-await

コールバック地獄→Promise

まずはコールバック地獄を
Promiseで解決した例を示しましょう。

この例ではタイマーを使った非同期処理です。

(setq lexical-binding t)
(defun print-delay-value (delay-sec format-string value next-callback)
  "DELAY-SEC秒後にVALUEをFORMAT-STRINGに従って表示させ、NEXT-CALLBACK関数にVALUEを渡す。"
  (run-at-time delay-sec nil
               (lambda ()
                 (message format-string value)
                 (funcall next-callback value))))
(defun callback-sample ()
  (interactive)
  (print-delay-value
   1 "first result: %s" 33
   (lambda (result)
     (print-delay-value
      1 "second result: %s" (* result 2)
      (lambda (second-result)
        (print-delay-value
         1 "third result: %s" (* second-result 2)
         #'ignore))))))

M-x callback-sampleを実行すると

  1. 1秒後に33と表示され
  2. また1秒後に33*2と表示され
  3. また1秒後に33*2*2と表示されます

print-delay-value
run-at-time 関数を使って値を表示し、
値をコールバック関数に渡します。

ここで lexical-binding
t に設定しておく必要があります。

なぜなら、タイマーに渡す関数から
引数を参照するためです。

だんだんとコールバックの入れ子が
深くなっていくのがわかりますね。

コードの流れが理解しづらいのです。

そこでpromise.elを使って書き直すと…

(require 'promise)
(setq lexical-binding t)
(defun promise-sample ()
  (interactive)
  (promise-chain (promise:delay 1 33)
    (then (lambda (result)
            (message "first result: %s" result)
            (promise:delay 1 (* result 2))))
    (then (lambda (second-result)
            (message "second result: %s" second-result)
            (promise:delay 1 (* second-result 2))))
    (then (lambda (third-result)
            (message "third result: %s" third-result)))))

M-x promise-sampleも同じ結果になります。

ここで登場する promise:delay
は一定時間後に値を返す関数です。

コードはすっきりしました。

Promise→Async/Await

Async/Awaitはさらに上をいきます。

同じ動作をするコマンドを書き直してみます。

(require 'async-await)
(setq lexical-binding t)
(async-defun async-await-sample ()
  (interactive)
  (let (result second-result third-result)
    (setq result (await (promise:delay 1 33)))
    (message "first result: %s" result)
    (setq second-result (await (promise:delay 1 (* result 2))))
    (message "second result: %s" second-result)
    (setq third-result (await (promise:delay 1 (* second-result 2))))
    (message "third result: %s" third-result)))

ここでM-x async-await-sampleを
実行すると同じ結果になります。

async/await/promiseを取り除いたら、

(defun sync-sample ()
  (interactive)
  (let (result second-result third-result)
    (setq result 33)
    (message "first result: %s" result)
    (setq second-result (* result 2))
    (message "second result: %s" second-result)
    (setq third-result (* second-result 2))
    (message "third result: %s" third-result)))

ごくごく普通のコードになってしまいました!!

逆に言えば、
Promiseを作ってawaitで受け取るようにすれば、
あっさり非同期化ができてしまうのです。

しかも今のpromise.elには
以下のようにPromiseを作る関数群が
たくさん用意されています。

promise:run-at-time
タイマーによる関数実行
promise:delay
遅延評価
promise:time-out
タイムアウト
promise:make-process
プロセス作成
promise:make-process-string
プロセス実行結果の文字列
promise:url-retrieve
URLにアクセスした結果の文字列
promise:xml-retrieve
URLにアクセスした結果のXMLオブジェクト
promise:async-start
async-start(async.el)による別プロセスで得た結果

プロセスもネットワークも
非同期処理が当たり前に書けてしまいます。

これってすごくないですか?

さらなるサンプルは
https://github.com/chuntaro/emacs-async-await/tree/master/examples/async-await-examples.el
を参照してください。

本サイト内の関連パッケージ


本日もお読みいただき、ありがとうございました。参考になれば嬉しいです。