EmacsのAsync/AwaitをCask使って試す
最近ジェネレータとかコルーチンとかをなんとなく理解できたところです。
promiseとasync/await
私は最近JavaScriptを書くようになって、promiseやasync/awaitによる非同期処理に触れました。 最初はさっぱりでしたが、今では慣れて、コールバックはうーんになってます。
ところで、以前Emacsでasync/awaitの実装をしていた記事を読んだことがありました。
まずEmacs lispでpromiseを実装して、TypeScriptの出力を参考にジェネレータの機能を用いてasync/awaitまで実装したようです。(async/awaitはpromiseのシンタックスシュガーというのは知っていたが、ジェネレータで実現できるのか...!)
ということで、これを試してみました。(サンプルを試すだけです)
環境
async-await.el
上の記事に書いてある通り、async-await.elのリポジトリにはサンプルコードがあります。ただ、それを試すには、package.elでMelpaからasync-awaitをインストールして、リポジトリにあるサンプルファイルをどこかにコピーして、そのファイル中の1文を評価する、という手順を踏むのでちょっと面倒です。
今回は試すだけなのと、どうしてもCaskが使いたかったので、Caskを使って試します。
準備
リポジトリのクローン
上のリポジトリを適当な場所で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のサンプルが何やってるかメモ。
- async/awaitを使わず同期的に実行。
- promise、async/awaitを使って非同期的に実行。
- 内容は2と同じ。コードが違う。
- 2の非同期関数と3の非同期関数と数値3を返すものを実行。
- 0.7秒を3回待つ関数と1秒を3回待つ関数を並列に実行。
- 2以上の値が引数に入るとrejectされるpromiseを実行してエラーを発生させる。
- 6にエラーをキャッチする処理を加えたものを実行。
- wikipediaからGNUとEmacsの記事を取得して、一部を表示。
async-await-examples.el
をasync
キーワードでgrep。- 内容は9と同じ。コードが違う。
- 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で与えた関数の実行を待たずに終了してしまうのを防ぐためみたいです。
検索していたみたら、上のような記事を見つけました。普通の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