promise 20170215.2204(in MELPA)
Promises/A+

概要

<2017-02-13 月> 更新!新関数追加によりとても使いやすくなりました!

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. 1秒後に33と表示させる
  2. また1秒後に33*2を表示させる
  3. また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.elPromises/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.elasync-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-racepromise: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.elconcurrent.el と同類ですが、
以下の相異点があります。

  • JavaScriptのPromiseの忠実な移植
  • 記述がより簡潔

<2017-02-13 月>現在のバージョンでは

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よりも使いやすくなっています。

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


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