つうさにメモブログ

つうさにがメモをブログとして書いていくところ

EmacsのAsync/AwaitをCask使って試す

最近ジェネレータとかコルーチンとかをなんとなく理解できたところです。

promiseとasync/await

私は最近JavaScriptを書くようになって、promiseやasync/awaitによる非同期処理に触れました。 最初はさっぱりでしたが、今では慣れて、コールバックはうーんになってます。

ところで、以前Emacsでasync/awaitの実装をしていた記事を読んだことがありました。

qiita.com

まずEmacs lispでpromiseを実装して、TypeScriptの出力を参考にジェネレータの機能を用いてasync/awaitまで実装したようです。(async/awaitはpromiseのシンタックスシュガーというのは知っていたが、ジェネレータで実現できるのか...!)

ということで、これを試してみました。(サンプルを試すだけです)

環境

async-await.el

上の記事に書いてある通り、async-await.elのリポジトリにはサンプルコードがあります。ただ、それを試すには、package.elでMelpaからasync-awaitをインストールして、リポジトリにあるサンプルファイルをどこかにコピーして、そのファイル中の1文を評価する、という手順を踏むのでちょっと面倒です。

今回は試すだけなのと、どうしてもCaskが使いたかったので、Caskを使って試します。

準備

リポジトリのクローン

github.com

上のリポジトリを適当な場所でgit cloneします。 以下、クローンしたemacs-async-awaitディレクトリで作業します。

$ git clone https://github.com/chuntaro/emacs-async-await.git
$ cd emacs-async-await
$ git checkout -b cask-examples
caskでパッケージのインストール

caskを使って、必要なパッケージを.cask/にインストールします。 まず以下のようなCaskファイルをリポジトリのルートに作ります。

(source melpa)

(package-file "async-await.el")

そしてcask installでパッケージとその依存をインストールします。

$ cask install

caskは実行の最初に結構なオーバーヘッドがあるので時間がかかるかもしれません。 ファイルはCaskのあるディレクトリの.cask/にインストールされるので、~/.emacs.d/下には影響はありません。(caskのインストール方法は今回話しません。GitHubのcask/caskを見ればすぐわかると思います。)

async-awaitパッケージはpromiseパッケージに依存し、promiseパッケージはasyncパッケージに依存してますが、caskはファイルのPackage-Requiresを見て依存するパッケージもインストールしてくれるので、Caskファイルにdepend-onを書く必要はありません。(これだけ見ると、npmとpackage.jsonとnode_modulesの関係とまんま同じですね)

async-await-examples.elを試す

リポジトリexamplesディレクトリにasync-await-examples.elがあります。これをコマンドラインから実行して試すには、普通-Lオプションによるload-pathの設定が必要で、これが面倒です。

caskではcask execを用いることで、caskでインストールしたパッケージをload-pathに設定してくれた状態でemacsを起動できます。cask exec時に設定されるload-pathはcask load-pathで確認できます。(caskはこんな感じに、elispパッケージ開発者にとって便利で、caskをinit.elで使うパッケージ管理ツールとしてしか知らない人に紹介したかった...)

ということで、cask execを使ってasync-await-examples.elを試すには以下のようにします。

$ cask exec emacs -q -l examples/async-await-examples.el --eval "(launcher)"

これで、caskを使ってasync-await-examples.elで提供されるサンプルを試すことができます。

(ただ、9,10,11については、ちゃんと動かすために少し書き換える必要があります。)

253行目
;; (message "grep result:\n%s" (await (make-grep-process "async" "async-await-examples.el")))
(message "grep result:\n%s" (await (make-grep-process "async" "examples/async-await-examples.el")))
 
260行目
                                          ;; "grep" "async" "async-await-examples.el")))
                                          "grep" "async" "examples/async-await-examples.el")))
どこかに追加
(require 'async)
async-await-examples.elの詳細

ファイルを見ればわかるけど、一応1~11のサンプルが何やってるかメモ。

  1. async/awaitを使わず同期的に実行。
  2. promise、async/awaitを使って非同期的に実行。
  3. 内容は2と同じ。コードが違う。
  4. 2の非同期関数と3の非同期関数と数値3を返すものを実行。
  5. 0.7秒を3回待つ関数と1秒を3回待つ関数を並列に実行。
  6. 2以上の値が引数に入るとrejectされるpromiseを実行してエラーを発生させる。
  7. 6にエラーをキャッチする処理を加えたものを実行。
  8. wikipediaからGNUEmacsの記事を取得して、一部を表示。
  9. async-await-examples.elasyncキーワードでgrep
  10. 内容は9と同じ。コードが違う。
  11. asyncパッケージの関数を使って、フィボナッチ数の30000番目を表示。

6と7はエラーが発生してビビるけど、そういうコードなので大丈夫。

test-async-await.elを試す

記事の中にtest-async-await.elというファイルが出てきます。次はこれを試します。

emacsのコマンドループ

記事の中で、これをbatchモードで実行するには(dotimes (_ 200) (sleep-for 0.01))を加えて実行する必要があるとしています。 これは、batchモードのEmacsがrun-at-timeで与えた関数の実行を待たずに終了してしまうのを防ぐためみたいです。

dev.ariel-networks.com

検索していたみたら、上のような記事を見つけました。普通のEmacsでは(recursive-edit)でコマンドループを実行しているけど、batchモードだとこれを実行しないみたいです。この記事では(recursive-edit)だとダメだったと書いてありますが、自分の環境では(recursive-edit)を一番後ろに足してtest-async-await.elを実行すれば、終了せず非同期に実行することができました。

ただ、これだと非同期関数が実行し終わった後もEmacsは終了せず、端末でC-cしなくてはなりません。ということで、非同期関数が実行し終わったらEmacsが終了してくれるように、whiteを使いつつtimer-list変数がnilになったらEmacsを終了するようにしました。

;;; -*- 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")

;; (recursive-edit)

(while t 
  (sleep-for 0 100)
  (unless timer-list
    (kill-emacs)))

上をexamples/test-async-await.elに保存します。cask execを使ってtest-async-await.elを試すには以下のようにします。

$ cask exec emacs --script examples/test-async-await.el

caskのオーバーヘッドがヤバイので一番最初の"pong"が表示されるまでちょっと長いですが、その後非同期的に実行されているのがわかります。

終わりに

elispパッケージのリポジトリで、cask execを使ってサンプルを実行したりすると、elisp開発者気分を味わえます。

ところで、非同期APIが主なJavaScriptではイベントループがありますが、Node.jsではどういう判定で終了しているのでしょうか。ファイルを読み終わった後に、待ち行列が空になったら終了するというものだったら、今回timer-listで終了しているのは、似たところがあるのかもしれません。

今回はasync-awaitパッケージのサンプルを試しただけだったので、自分でコードを書いてみたりもしたいですね。

作業後のディレクト

$ tree
.
├── Cask
├── README.md
├── async-await.el
└── examples
    ├── async-await-examples.el
    └── test-async-await.el

1 directory, 5 files