17
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Emacs に JavaScript の Promise と Async/Await を移植した時の苦労話など

Last updated at Posted at 2017-12-14

#はじめに

先日、Emacs に JavaScript の Promise と Async/Await を移植しました。
MELPA にアップされているので promise と async-await で検索してインストールしてみてください。
これには examples フォルダが含まれていないので以下から clone すると examples も取得出来ます。

https://github.com/chuntaro/emacs-promise
https://github.com/chuntaro/emacs-async-await

promise.elasync-await.el の基本的な使い方については、以下のるびきちさんの紹介ページを参照してみてください。
http://emacs.rubikitch.com/promise/
http://emacs.rubikitch.com/async-await/

examples フォルダにも一通りのサンプルコードがあるので、使う前には目を通しておく事をお勧めします。
しかもインタラクティブに実行出来るランチャーも仕込んであるのでコメント内にある起動コードの末尾で C-x C-e すると新規に Emacs を起動してランチャーが実行されます。
なので、サンプルコードを見ながらすぐに実行して試す事が出来ます。
↓これがコメント内に書いてあります。

;; (start-process "emacs" nil (file-truename (expand-file-name invocation-name invocation-directory)) "-Q"  "-Q" "--execute" "(package-initialize)" "-L" (concat default-directory "../") "-l" (buffer-file-name) "--execute" "(launcher)")

(環境に依存しないようになってるはずですが、起動コードがちゃんと動かなかったらごめんなさい…)

#動機

さて、紹介はこれぐらいにして実装の動機について書こうかと思います。
ちなみに Promise/A+ と Async/Await は何から移植したかというと、Promise/A+ は以下の実装を
https://github.com/then/promise
Async/Await は TypeScript 版(正確にはコンパイル後の JavaScript コード)を移植しました。

まず、以下の TypeScript で Async/Await を使ったコードとコンパイル後の JavaScript のコードを見てください。

test-async-await.ts
function delay(ms: number) {
  return new Promise(resolve => setTimeout(resolve, ms));
}

async function ping() {
  for (var i = 0; i < 5; i++) {
    await delay(300);
    console.log("ping");
  }
}

async function main() {
  await ping();
}

main();
console.log("pong");
$ tsc --target ES2015 test-async-await.ts

でコンパイルして出力された JavaScript コードは以下です。(そのままコピペしてます)

test-async-await.js
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
    return new (P || (P = Promise))(function (resolve, reject) {
        function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
        function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
        function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); }
        step((generator = generator.apply(thisArg, _arguments || [])).next());
    });
};
function delay(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
}
function ping() {
    return __awaiter(this, void 0, void 0, function* () {
        for (var i = 0; i < 5; i++) {
            yield delay(300);
            console.log("ping");
        }
    });
}
function main() {
    return __awaiter(this, void 0, void 0, function* () {
        yield ping();
    });
}
main();
console.log("pong");
$ node test-async-await.js

と実行すると

pong
ping
ping
ping
ping
ping

pong の後に ping が5個ポツポツ表示されて終わるだけのコードですが setTimeout() という非同期 API を使ったコードであるにも関わらず for 文を使って普通に同期的にコードが書けてます。
main() 以降が非同期で実行されているのは pong が先に表示されてるので分かります。
簡単に非同期コードが書ける Async/Await は素晴らしいですね~!

ところで JavaScript の方を見てみると、どうやら Async/Await は yield (ジェネレータ)を使ってエミュレートされているようです。
少しわざとらしいですが… ES2015 にはジェネレータはあっても Async/Await が存在しないので TypeScript コンパイラがそのようにコンパイルします。
ES2017 でコンパイルすると Async/Await が存在するのでエミュレーションコードはバッサリなくなり TypeScript と JavaScript でほとんど同じコードが出力されます。

最初このコードを見た時はジェネレータがあれば Async/Await をエミュレート出来るんだなぁ程度でしたが、Emacs25.1 がリリースされた時に NEWS ファイルを見ていると generator.el の記述が!
これなら Emacs Lisp で Async/Await が実装出来るのではないか?と generator.el や Emacs Lisp 用の Promise について色々調べてみました。
Emacs で動く Promise は Github で1つ見つけましたが、コードがどうにも好きになれませんでした。
then などのプリフィックスの無い関数がグローバルに定義されてしまってて、elisp 的には良くないコードになっていました。(他にも直したい部分は幾つもありました)
ただし promise-chain の書き方など少し参考にさせてもらいました。

それと Promise というのは基本的にただのライブラリですが、似たような実装を統一する目的で以下で仕様が厳密に定義されています。(仕様の名前が Promise/A+ と言います)

これは JavaScript 用の仕様ですが、例え elisp で実装されてようが Promise を名乗るからにはこの仕様になるべく沿っていて欲しかったというのもあります。

という事で、自分で Promise を実装する事にしました。
ちなみに Promise は以下のサイトが総本山です。

このサイトからリンクが張ってある、前述の

https://github.com/then/promise (以下 then/promise)

から JavaScript コードをそのまま、出来る限り機械的に elisp に移植する事を試みました。
そうすれば Promise/A+ の仕様にちゃんと沿った実装になるはずです。

その際に苦労した点などをこの後書いて行こうと思いますが、なにしろネタがネタだけに(多分)マニアックな内容になっている事をご了承ください…。

#実装 (Promise 編)

まず Promise 自体を知るには以下のサイトの「JavaScript Promiseの本」が概要や使い方について分かり易かったです。

以下も分かり易いかもしれません。
https://developers.google.com/web/fundamentals/primers/promises
(以下のサイトから辿りました)
https://www.d-wood.com/blog/2017/02/11_8804.html

使い方が分かれば、その後 Promise がどのように実装されているかを知る必要がありますが、以下のサイトが
Promise を1から実装しながら分かり易く解説しています。

日本語で Promise の実装を解説している記事はここぐらいしか見当たりません。
これは then/promise と似た実装を行なっている為、この後 then/promise を読む助けには(多少…)なります。
実際は非同期ライブラリの性か then/promise のコードをちゃんと理解するのは(少なくとも自分は)容易ではありませんでした…。
とにかくライブラリ内に console.log() を書きまくって流れを理解してたような記憶があります。

then/promise の肝は core.js です。このソースだけで Promise/A+ の仕様を実装しています。
他のソースは es6-extensions.js のように core.js で定義された Promise を使って便利な API を追加したものです。

then/promise は JavaScript のクラス機能を使っていてコンストラクタも必要だったので elisp でも eieio(elisp 版 CLOS) のクラス(defclass)を使いました。
更にクラスにする事で Promise クラスの継承もサポート出来ます。(example18 参照)

また defstructdefclass を使う事で Promise/A+ の thenable なオブジェクトの仕様を満たす事が出来ます。(example17 参照)
thenable なオブジェクトを作れると何が嬉しいかというと、defstructdefclass を使った別の実装の非同期処理 API と then メソッド(promise.el では promise-then メソッド)を介して一緒に使う事が出来るという事ですが、elisp にそのようなライブラリは無いので今のところ存在価値はほとんど無いかもしれません…。
もちろん普通に promise-new 関数で作成したオブジェクト(promise-class のインスタンス)は thenable です。

core.js が理解出来て defclass で Promise クラスを実装する事を決定すれば、後は機械的に置き換えていけばいいはずですが、当然 JavaScript と elisp 間での互換性が無い部分が少しあるので、それを解決しなければいけません。

まず最初に問題になったのが type-of です。
elisp の type-of はプリミティブな型しか判別しません。

(type-of 1) ; => integer
(type-of 1.0) ; => float
(type-of "a") ; => string

(cl-defstruct test x y z)
(type-of (make-test)) ; => vector ← Common Lisp だとちゃんと TEST が返る

elisp は defstructdefclass のインスタンスは vector に格納されるので vector が返されるだけなのです。
しかし promise.el を実装する上で defstructdefclass されたインスタンスの型が分からないのは移植する上で致命的だったので何とか実装したものが promise--type-of です。

promise-core.el
(defun promise--type-of (obj)
  (cond
   ((not (vectorp obj))
    (type-of obj))
   ((cl-struct-p obj)
    ;; Code copied from `cl--describe-class'.
    (cl--class-name (symbol-value (aref obj 0))))
   ((eieio-object-p obj)
    (eieio-object-class obj))
   (t
    'vector)))

これで、今のところちゃんと動いてます。

(cl-defstruct test1 x y z)
(promise--type-of (make-test1)) ; => test1

(defclass test2 () ())
(promise--type-of (make-instance 'test2)) ; => test2

Promise の実装に必要な機能として asap というものがあります。
これは As Soon As Possible の略で、渡された関数を出来る限り早く非同期で実行しろというものです。
Promise は渡された関数がいきなり値を返すような同期関数だったとしても、非同期で実行しなければいけないとなっています。
なのでタイマー関数に適当な時間を渡して実行すればいいのですが、適当な時間という曖昧なものじゃなくて非同期かつ最速で実行して欲しいものです。
その為に Node.js には process.nextTick() という API がありますが(ブラウザの場合は似たような内部関数)、elisp にはありません。
なので適当な時間で実行しますが、この時間をどの程度にすればいいか何か目安がないかと思案していたところ elisp で実装された有名な非同期処理ライブラリの deferred.el で 0.001 秒と定義されていたのでそれをそのまま使いました。

それが promise--asap です。

promise-core.el
(defun promise--asap (task)
  (run-at-time 0.001 nil task))

他は new Handler(...) で作られるオブジェクトは elisp では連想リストにしたぐらいです。
移植が終わってちゃんと動いてるから言える事ですが、改めて見ると実は上記以外は意外と JavaScript から機械的に elisp へ変換出来ていました。

#実装 (Async/Await 編)

Promise が移植出来たので、次は Async/Await (async-await.el) です。
これは TypeScript からコンパイルされた JavaScript 内の __awaiter を移植し、async 関数として定義された関数内の await をジェネレータの呼び出しに変更すればいい事になります。
最終的に前出の test-async-await.ts は以下のようになります。

test-async-await.el
;;; -*- lexical-binding: t; -*-

(require 'async-await)

(defun console.log (obj)
  (princ obj)
  (terpri))

(defun delay (ms)
  (promise-new (lambda (resolve _reject)
                 (run-at-time (/ ms 1000.0)
                              nil
                              (lambda ()
                                (funcall resolve ms))))))

(async-defun ping ()
  (dotimes (i 5)
    (await (delay 300))
    (console.log "ping")))

(async-defun main ()
  (await (ping)))

(main)
(console.log "pong")
(load "/path/to/test-async-await.el")

を評価して実行すると、全く同じ実行結果になります。
emacs --batch で実行しようとすると run-at-time の実行を待たずに一瞬で終了してしまうので、少し長くなりますが以下のようにする必要があります。

$ emacs --batch --eval '(package-initialize)' -l test-async-await.el --eval '(dotimes (_ 200) (sleep-for 0.01))'

ではここで前出の __awaiter を整形してみます。(見づらくなる記述は直しています)

var __awaiter = function (thisArg, _arguments, generator) {
  return new Promise(function (resolve, reject) {
    function fulfilled(value) {
      try {
        step(generator.next(value));
      } catch (e) {
        reject(e);
      }
    }
    function rejected(value) {
      try {
        step(generator["throw"](value));
      } catch (e) {
        reject(e);
      }
    }
    function step(result) {
      result.done
        ? resolve(result.value)
        : new Promise(function (resolve) {
          resolve(result.value);
        }).then(fulfilled, rejected);
    }
    step((generator = generator.apply(thisArg, _arguments || [])).next());
  });
};

promise.el を手に入れた今となっては、これの移植は簡単そうに見えますが幾つか問題はありました。

まず、Emacs 用のジェネレータである generator.el が JavaScript のジェネレータとは大分挙動が違うというのがあります。
ちなみに Emacs のジェネレータについては以下のサイトが大変参考になります。
https://qiita.com/kawabata@github/items/239345c38c431e1feb7d

JavaScript generator.el
終了判定 next() が返すオブジェクトの done の真偽値 エラー iter-end-of-sequence を発生させる
例外のスロー ジェネレータの throw() メソッド 無い

なので、コードは少し微妙なものになりましたが、以下のように elisp に __awaiter を移植しました。

async-await.el
(defun async-await--awaiter (iterator)
  (promise-new
   (lambda (resolve reject)
     (cl-labels ((fulfilled (value)
                            (condition-case reason
                                (step (iter-next iterator value))
                              (iter-end-of-sequence (funcall resolve (cdr reason)))
                              (error (funcall reject reason))))
                 (rejected (value)
                           ;; Please implement `iter-throw'!
                           ;; Even if you raise an exception here, Promise will be swallowed.
                           ;; Therefore, it is included in the return value and propagated.
                           (condition-case reason
                               (step (async-await--iter-throw iterator value))
                             (iter-end-of-sequence (funcall resolve (cdr reason)))
                             (error (funcall reject reason))))
                 (step (result)
                       (promise-chain (promise-resolve result)
                         (then #'fulfilled #'rejected))))
       (condition-case nil
           (step (iter-next iterator))
         (iter-end-of-sequence nil))))))

次に JavaScript の async functionasync-await.el では async-defun ですが、これが行う仕事は渡されたコード内の awaityield の呼び出しに変換しコード全体をジェネレータとして定義し __awaiter の呼び出しに変換する事です。
なので await をローカルのマクロとして定義する cl-macrolet を使う事までは分かるのですが、自分のスキルではどうやっても目的の結果になりませんでした…。

完全に挫折してましたが、ふと generator.el のコードを見ている内に似たような事をしてるコードを見つけたので、目的に合うように直して実装したところうまく行きました。
そのコードがこれです。(抜粋)

async-await.el
(defmacro async-defun (name arglist &rest body)
  (let* ((parsed-body (macroexp-parse-body body))
         (declarations (car parsed-body))
         (exps (macroexpand-all
                `(cl-macrolet
                     ((await (value)
                             `(iter-yield ,value)))
                   ,@(cdr parsed-body))
                macroexpand-all-environment)))
    `(defun ,name ,arglist
       ,@declarations
       (async-await--awaiter
        (funcall (iter-lambda () ,exps))))))

多分常套句なのでしょうね…。いきなりこんなコード絶対書けないので助かりました。
cl-macroletawaititer-yield に変換し、コード全体を iter-lambda でジェネレータとして定義し async-await--awaiter の呼び出しに変換しています。

という事で、これでめでたく Emacs 用の Async/Await を実装出来ました!

#最後に

今のところ、他の人がこれを使ってるのを見た事がありません(笑)
そもそも自分で使えよという話ですが、文字コードの変換ですら非同期 API を使わないといけない JavaScript と比べて Emacs は非同期処理を連鎖させる事などほとんどありません。
ほとんどが同期関数でネットワークアクセスなどの時間の掛かる処理は非同期関数がありますが、単にコールバック関数を登録すれば済むワンショットの処理ばかりです。
ただ Emacs26 からはスレッド API が追加されるので、少しずつ状況は変わるのかもしれません。
elisp で非同期 API を連鎖させなければならなくなり、コールバック地獄に陥った時に async-await.elpromise.el なんてものがある事を思いだしてもらえたら幸いです。

おわり

更新履歴
2017/12/15 14:50 日本語の誤字修正

17
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
17
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?