- promise 20170215.2204(in MELPA)
- Promises/A+
目次
- 概要
- タイマーによる非同期処理
- そこでPromiseですよ!
- 基本的な使い方
- エラーをcatchする
- promise:run-at-time:関数を遅延実行する
- promise:make-process:プロセスを実行する
- promise:make-process-string:プロセス実行結果の文字列を得る
- promise:url-retrieve:URLにアクセスする
- promise:xml-retrieve:URLにアクセスしたXMLを得る
- promise:async-start:別のEmacsプロセスで結果を得る
- promise-race:早い者勝ち
- promise:time-out:タイムアウトを記述する
- promise-all:すべての処理を待つ
- deferred.elと比べてみる
- 作者による使用例
- まとめ
- 参考サイト
概要
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)による別プロセスで得た結果
JavaScript界隈では
Promiseというイケてる非同期処理が
アツいですよね。
本来、非同期処理を記述するには
コールバックを書きますが、
コードが難読化してしまう
という欠点があります。
そこで救世主となるのがPromiseです。
http://qiita.com/koki_cheese/items/c559da338a3d307c9d88 より
Promiseを使ってない場合だと非同期のメソッドを繋げる場合
いわゆるコールバック地獄となってしまいます。
//Promiseを使わない非同期を繋げる場合 A(function(a){ B(a, function(b){ C(b, function(c){ done(c); // ABC }); }); });でもPromiseを使えばメソッドチェーンにすることができコールバック地獄を回避することができます。
A().then(B).then(C).then(done); // ABC
実はEmacs Lispでも
以下の非同期処理が記述できます。
- タイマー
- プロセス
- ネットワーク
- スレッド!(Emacs26より)
インストール
パッケージシステムを初めて使う人は
以下の設定を ~/.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/")))
初めてpromiseを使う方は
以下のコマンドを実行します。
M-x package-install promise
アップグレードする方は、
以下のコマンドでアップグレードしてください。
そのためにはpackage-utilsパッケージが必要です。
M-x package-install package-utils (初めてアップグレードする場合のみ) M-x package-utils-upgrade-by-name promise
タイマーによる非同期処理
では、簡単な例として、
タイマーを使った非同期処理を書いてみましょう。
- 1秒後に33と表示させる
- また1秒後に33*2を表示させる
- また1秒後に33*2*2を表示させる
print-delay-value
は
run-at-time
関数を使って値を表示し、
値をコールバック関数に渡します。
ここで lexical-binding
を
t
に設定しておく必要があります。
なぜなら、タイマーに渡す関数から
引数を参照するためです。
(はあ… lexical-let
を使う必要があった頃は醜くて死んでた…)
;;; -*- 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)))) (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)))))
実行してみると、
*Message*バッファに1秒ごとに
first result: 33 second result: 66 third result: 132
と表示されます。
ただ、、、
だんだんとコールバックの入れ子が
深くなっていくのがわかりますね。
コードの流れが理解しづらいのです。
ここでは一回の
タイマー呼び出ししかしていませんが、
ネットワークアクセスなどが絡むと
本当にやっかいです。
そこでPromiseですよ!
ここでPromiseを導入してみます。
promise:delay (SEC VALUE)
は
SEC秒後にVALUEのpromiseを返します。
;;; -*- lexical-binding: t -*- (require 'promise) (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))))
すっきり整理されたではありませんか!!!
テストだって楽に書けてしまいます。
(ert-deftest promise-async-test () (let (expected actual) (promise-chain (promise:delay 1 33) (then (lambda (result) (push 33 expected) (push result actual) (promise:delay 1 (* result 2)))) (then (lambda (second-result) (push 66 expected) (push second-result actual) (promise:delay 1 (* second-result 2)))) (then (lambda (third-result) (push 132 expected) (push third-result actual)))) (sit-for 3.1) (should (equal expected actual))))
基本的な使い方
promise.el
は Promises/A+ の忠実な移植です。
基本的には
promise-new
promise-chain
then
で記述します。
promise-new
は、2つの2引数関数
resolve
reject
を引数に取ります。
resolve
は処理が成功したときにreject
はエラーが起きたときに
呼び出します。
promise-chain
は
最初にpromiseオブジェクトを渡し、
後に then
を取っていきます。
then
は、
値を引数とする関数を引数に取ります。
エラーをcatchする
then
は、
第2引数の関数によって
エラーが起きたときの処理も記述できます。
(promise-chain (promise:delay 1 33) (then (lambda (result) (message "first result: %s" result) (setq a-dummy (/ 1 0)))) ; An `(arith-error)' occurs here. (then (lambda (second-result) (message "second result: %s" second-result) (promise:delay 1 (* second-result 2))) (lambda (reason) (message "catch the error: %s" reason))))
実行すると
first result: 33 catch the error: (arith-error)
と表示されます。
promise:run-at-time:関数を遅延実行する
promise:delay
は値を返しますが、
promise:run-at-time
は関数を実行します。
promise:delayの一部を
promise:run-at-timeに置き換えてみます。
(promise-chain (promise:delay 1 33) (then (lambda (result) (message "first result: %s" result) (promise:run-at-time 1 '* result 2))) (then (lambda (second-result) (message "second result: %s" second-result) (promise:run-at-time 1 '* second-result 2))) (then (lambda (third-result) (message "third result: %s" third-result))))
promise:make-process:プロセスを実行する
promise:make-process
は
プロセス(プログラム)を実行します。
プロセスオブジェクトを返しますので、
実行結果を表示するには
process-buffer
と
display-buffer
を使います。
(promise-chain (promise:make-process "sh" "-c" "sleep 1; echo OK") (then (lambda (proc) (display-buffer (process-buffer proc)))))
実行すると1秒後に
OKと表示されたバッファが
ポップアップします。
promise:make-process-string:プロセス実行結果の文字列を得る
promise:make-process-string
は
promise:make-process
と同様に
プロセスを実行しますが、
文字列を返す点が異なります。
こっちの方が手軽ともいえます。
(promise-chain (promise:make-process-string "sh" "-c" "sleep 1; echo OK") (then (lambda (output) (message "%s" output))))
実行すると1秒後に
OKとエコーエリアに表示されます。
promise:url-retrieve:URLにアクセスする
promise:url-retrieve
は
URLにアクセスし、その内容を返します。
(promise-chain (promise:url-retrieve "https://httpbin.org/ip") (then (lambda (output) (message "%s" output))))
実行すると自分のIPアドレスが
JSON形式でエコーエリアに表示されます。
promise:xml-retrieve:URLにアクセスしたXMLを得る
promise:xml-retrieve
はURLにアクセスし、
S式化したXMLを得ます。
(promise-chain (promise:xml-retrieve "https://httpbin.org/xml") (then (lambda (output) (pp-display-expression output "*xml-output*"))))
実行すると*xml-output*バッファが
ポップアップし、このような結果が出ます。
((slideshow ((title . "Sample Slide Show") (date . "Date of publication") (author . "Yours Truly")) "\n\n " (slide ((type . "all")) "\n " (title nil "Wake up to WonderWidgets!") "\n ") "\n\n " (slide ((type . "all")) "\n " (title nil "Overview") "\n " (item nil "Why " (em nil "WonderWidgets") " are great") "\n " (item nil) "\n " (item nil "Who " (em nil "buys") " WonderWidgets") "\n ") "\n\n"))
promise:async-start:別のEmacsプロセスで結果を得る
async.el の async-start
は
Emacsの子プロセスを立ち上げて関数を実行します。
promise:async-start
はそのpromise版です。
async-startの例では
(async-start ;; What to do in the child process (lambda () (message "This is a test") (sleep-for 3) 222) ;; What to do when it finishes (lambda (result) (message "Async process done, result should be 222: %s" result)))
があります。
実行すると3秒後にはエコーエリアに
Async process done, result should be 222: 222
と表示されます。
これをpromiseを使って書き換えると、
次のようになります。
(promise-chain (promise:async-start ;; What to do in the child process (lambda () (message "This is a test") (sleep-for 3) 222)) (then (lambda (result) ;; What to do when it finishes (message "Async process done, result should be 222: %s" result))))
実行結果は同じです。
prpmise:async-startはasync-startと
引数の互換性があるため、
async-startのコールバックも記述できます。
(promise-chain (promise:async-start ;; What to do in the child process (lambda () (message "This is a test") (sleep-for 3) 222) ;; What to do when it finishes (lambda (result) (message "Async process done, result should be 222: %s" result))) (then (lambda (result2) (sit-for 1) (message "%s" result2))))
実行すると3秒後にはエコーエリアに
Async process done, result should be 222: 222
と表示され、さらに1秒後に
222
と表示されます。
あくまでもpromiseに渡されるのは
子プロセスでの結果であることに注意してください。
promise-race:早い者勝ち
promise-race
で
一番早い処理のみ実行させられます。
(promise-chain (promise-race (vector (promise:delay 2 "2 seccods") (promise:delay 1 "1 second") (promise:delay 3 "3 secconds"))) (then (lambda (result) (message "result: %s" result))))
実行すると
result: 1 second
と表示されます。
promise:time-out:タイムアウトを記述する
promise-race
と promise:time-out
で
タイムアウト処理が記述できます。
(promise-chain (promise-race (vector (promise:time-out 2 "time out") (promise:delay 3 "3 seconds"))) (then (lambda (result) (message "result: %s" result)) (lambda (reason) (message "promise-catch: %s" reason))))
実行すると
promise-catch: time out
と表示されます。
promise-all:すべての処理を待つ
promise-all
で並行処理をし、
すべての処理が終了するまで待ちます。
(promise-chain (promise-all (vector (promise:delay 2 "2 seccods") (promise:delay 1 "1 second") (promise:delay 3 "3 secconds"))) (then (lambda (results) (message "result[0]: %s" (aref results 0)) (message "result[1]: %s" (aref results 1)) (message "result[2]: %s" (aref results 2)))))
実行すると3秒後に
result[0]: 2 seccods result[1]: 1 second result[2]: 3 secconds
と一度に表示されます。
deferred.elと比べてみる
最後に、既出の非同期ライブラリ
deferred.el と比べてみます。
;;; -*- lexical-binding: t -*- (require 'deferred) (defun do-something-deferred (delay-sec value) (deferred:$ (deferred:wait (* 1000 delay-sec)) (deferred:nextc it (lambda (x) value)))) (deferred:$ (deferred:next (lambda () (do-something-deferred 1 33))) (deferred:nextc it (lambda (result) (message "first result: %s" result) (do-something-deferred 1 (* result 2)))) (deferred:nextc it (lambda (second-result) (message "second result: %s" second-result) (do-something-deferred 1 (* second-result 2)))) (deferred:nextc it (lambda (third-result) (message "third result: %s" third-result))))
処理順に記述できるものの、
いくぶん複雑になっています。
作者による使用例
まとめ
promise.el
は内部でタイマーを使った
Promises/A+ の忠実な移植+Emacs特化関数群です。
Promiseを使うことでEmacs Lispで
非同期処理がとても書きやすくなります。
既存の deferred.el
や concurrent.el
と同類ですが、
以下の相異点があります。
- JavaScriptの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)による別プロセスで得た結果
とEmacs専用関数が充実し、今では
deferred.elよりも使いやすくなっています。
本サイト内の関連パッケージ
- async - Asynchronous processing in Emacs
- deferred - Simple asynchronous functions for emacs lisp
- async-await - Async/Await
参考サイト
本日もお読みいただき、ありがとうございました。参考になれば嬉しいです。