読者です 読者をやめる 読者になる 読者になる

deferred.el について補足いろいろ

こちらの記事:deferred.elでHTTP通信を非同期化する やこちらの記事:Org-modeとToodledoを連携させるorg-toodledo.elを非同期実行に対応させましたを見て大変感動いたしまして、deferred.el 作った人としていくつか補足したいと思い、この記事を書きました。

deferred.el 情報、サンプルコード

まず、ドキュメントやサンプルコードが少ないという問題ですが、自分の力不足で申し訳ありません。今のところ、まとまっている情報は以下です。

あとは、MELPAで deferred.el への依存を調べてコードを読むというのもあります。

非同期/同期コードの分離、デバッグ

deferred.el は async.el のような一箇所だけ非同期で動かすようなツールと違って、小さな非同期タスクをつなげて大きな処理を組み立てるという目的のライブラリです。 そのため、書いてるうちに非同期タスク連鎖が長くなってしまって、さらにエラーのわかりにくさと合わさってデバッグが大変になることがあります。

そこで、ぜひおすすめしたいのは、deferred関数による非同期タスクの連結部分と、同期的な実務処理を分けることです。具体的には以下のようです。

;; 同期処理の関数を定義
(defun some_task_function_1 (arg)
    :
    result
)

(defun some_task_function_2 (arg)
    :
    result
)

;; 非同期処理をつなぐ部分
(deferred:$
  (deferred:process "some_command")
  (deferred:nextc it some_task_function_1)
  (deferred:nextc it some_task_function_2)
    :
)

some_task_function_1some_task_function_2 は、前の結果を受け取って何か処理を行う同期的な関数です。普通の関数ですので、通常のテストを書いたり scratch バッファで試すことも出来ます。また、 edebugブレークポイントを仕掛けてデバッグすることも出来ます。

このように分離しておくことで、deferredの連鎖は制御構造に集中できますし、見通しも良くなります。

concurrent.el の利用を検討する

deferred.el を使うことで、同期で書いていた処理を非同期な処理に機械的に書き換えることが出来ます。多くの問題はこれでほとんど解決します。

しかしながら、非同期の処理流れが並行して動いて、しかも協調的に動作(いわゆるマルチスレッド)することが求められる場合、このやり方では解決できなくなります。同期的なプログラミングスタイルのまま、安直な協調の方式としてグローバル変数を使ったフラグ制御をやりだすと、各地の処理がグローバルな状態に依存してしまうため、大きな処理を書くとすぐに破綻します。

そういうケースでは並行プログラミングの設計を行う必要があるのですが、大抵の場合、並行動作を想定せずに書いた既存のコードは大幅な書き直しになります。並行プログラミングについて復習してがんばりましょう。

並行処理の設計を行うと、並行プログラミング特有の汎用的な部品が欲しくなります。いくつかの部品を concurrent.el に用意しました。例えば、golangのchannelに対応するものとして cc:signal があったり、外部プロセス数の制御用として cc:semaphore があります。あとはqueueとかあると良さそうですよね。

設計のポイントとしては、処理の流れでプログラムを組み立てるのではなくて、データの流れを考えてその通り道を並行プログラミングの部品でつなげていくという感じになります。

評価タイミングの意識

deferred.el を使うと、実行タイミングの意識が大事になって来ます。具体的には以下のタイミングがあります。

  • コンパイル時(マクロ展開のタイミング)
  • 関数実行時
  • deferredタスク実行時

コンパイルはあんまり気にしなくても大丈夫だと思いますが、deferred関数と一緒に独自マクロを使いたい場合は評価順序に気をつける必要があります。

関数実行時が通常意識する実行タイミングです。静的なdeferredの連結はこのタイミングで行われます。

deferredタスク実行時は、deferredで作ったタスクが実際に実行されるタイミングで、上の関数実行時とは別のタイミングです。

はまりどころとしては、非レキシカルスコープ変数を参照しようとして未定義変数アクセスでエラーになるとか、実行時に評価されるはずが間違ってdeferredタスク実行時に評価されていたとか、カレントバッファとかポイント位置が実行時とdeferredタスク実行時で違うとかがあります。

レキシカルスコープ問題

実行時の値をdeferredタスクに透過的に渡したい場合は、レキシカルスコープ変数を使います。

Emacs 24 以上であれば、ファイル先頭の lexical-binding: t で有効になります。Emacs 23 以下で動かす必要がある場合は (require 'cl) して lexical-let を使います。

deferred.el本体は lexical-let で書いているのですが、clライブラリは Emacs 24 ぐらいから非推奨になっているので、今後どこかのタイミングで本体最新版をlexical-bindingで書き換えて、Emacs 23 以下のサポートを切り離す計画があります。

再利用問題

JavaScript の Promise は resolve複数回呼ぶことが出来ませんが、値を先に resolve した後からでも then をつなげることが出来ます。

deferred.el が参考にした JSDeferredcall (Promiseのresolveに相当) を何度も呼ぶことが出来ますが、next で先にタスクをつなげておかないと、値が流れて無くなってしまいます。

これは設計の違いですので、どちらが正しいというものではないと思います。

deferred.el は、 deferred:callback-post (Promiseのresolveに相当) を何度も呼ぶことが出来ますし、後から deferred:nextc でタスクをつなげることも出来ます。ただし、2度目以降の deferred:callback-post によって初回の値が上書きされますので、基本的にはdeferredタスクが何度も呼ばれないような作りにしておくのが安全です。

キャンセル問題

deferred:cancel は、意図通りに動かない場合があります。

実際にキャンセルの処理が動くのは deferred:wait などのタイマー、 deferred:processなどの外部プロセス、 deferred:url-retrieveなどで直接返ってきたdeferredオブジェクトだけです。

他の一般のdeferredオブジェクトのcancelは、呼ばれたdeferredオブジェクトより下流に値が伝搬しないように、タスクの連鎖を切るという実装になっています。ですので、deferredタスクの連鎖の仕方とcancelを呼ぶ対象によっては、cancelを呼んでも副作用が止まらない場合があります。

確実に副作用をキャンセルさせたい時は、プログラマが自分でキャンセル処理を書く必要があります。

deferred:do マクロの予定

JSだとメソッドチェーンで非同期タスクを楽につなげていけるのですが、 deferred.el ではメソッドチェーンが使えないので deferred:$deferred:nextc でタスクをつなげていく方法にしました。

ClojureのThreadマクロのように書けるとかっこ良かったのですが、タスクを入れ子にするなどの複雑な構造が扱いづらかったり、また見た目的にタスク間のつながりが把握しづらいように思ったので、当時は採用しませんでした。そこで、itを毎回書くことでタスクの接続位置を明確にできる(と思った)現在の方法を採用しました。

この方法は冗長なのでもっと短く書ける方法を試行錯誤した結果、edbi.elで使ったedbi:seqが良かったので、このマクロを deferred:do として採用するつもりでいます。

deferred:doの例:

;; シェルを順番に実行して結果を加工して、最終的にエコー領域に表示する。
;; (※deferred:doはまだありませんが、edbi:seqと同じです)
(lexical-let (aa bb)
  (deferred:do
   (aa <- (deferred:process-shell "ls /var | wc -l"))
   (bb <- (deferred:liftd length (deferred:process-shell "ls /lib")))
   (lambda (x) 
     (message "var: %s / lib: %s" aa bb))))

おわりに

とりあえず、ざっくばらんに書きました。 皆様の deferred.el ライフ向上の参考になれば幸いです。