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

レガシーコード生産ガイド

私に教えられることなら

(Biwa)Schemeで限定継続を使って非同期処理を書こうとした日記

以下全部あやふやな知識に基づいているので凄い人などが気まぐれにパーペキでわかりやすいの書いてくれるのを望む。

Schemeやっぱり面白いな〜といじってて、そういえばSmalltalk調べてたときに、Schemeの継続はアクター理論?のアクタへのメッセージパッシング?を実現するために考えられたんだっけ?と思い出し、継続をしばらく復習してから、次のようなものを書こうとした。

(define (test-agent msg cont)
  (なんか非同期処理して引数のthunkを評価する (lambda () (cont msg)))

(define (test-async)
  (async
   (print "before send")
   (print (send test-agent "hello"))  ; ここで非同期処理
   (print "after send"))
  (print "wait"))

test-agentは非同期処理を行うんだけど、それが返す値をこういう風に処理できればコールバック地獄に陥らなくて便利じゃん、という考え。Clojureのasyncとかも似たカンジなのかな。

(test-async)を呼び出すと次のような結果になってほしい。

before send
wait
hello
after send

最初は、sendが返ってきて以降の継続を渡せばいいのでは、と思ったんだけど、次のような結果になってしまった。

before send
wait
hello
after send
wait

send以降全ての継続なので、当たり前だけどwait以降まで含めて全て処理してしまう。

次コード中のこ↑こ↓で継続が切れてトップレベルまで戻って欲しいんだけど、方法がわからない。

(define (test-async)
  (async
   (print "before send")
   (print (send test-agent "hello"))
   (print "after send")) ; こ↑こ↓
  (print "wait"))

asyncでcall/ccを呼んだり、トップレベルの継続を使って脱出しようとしたりいろいろしたんだけど、そういえば限定継続なるものがあったっけ、と思い出して調べてみると、どうやら望む動作をするっぽい。

やってることはよくわからないけど、Final Shift for Call/cc: Direct Implementation of Shift and Resetという論文のセクション2にSchemeでのcall/ccを用いた実装例があるので拝借することにした。

話が前後するけど、最初Gaucheで試してて、非同期処理をどうすればいいのかわからなくなった。gauche.threadsが使えるっぽいけど、スレッドの終了を待とうとしたら無限ループか何かに入ってしまった。

そのままGaucheでやる方法を調べても良かったんだけど、最終的にJavaScriptのXHRをうまく書けるようになるとイカすな、と思ったのでBiwaSchemeを試してみることにした。BiwaSchemeライブラリ中のhttp-request関数などは最初から同期的に書けるようになっているので、今回は簡単にdocument.bodyへのクリックで試す。

また、define-syntaxの代わりにdefine-macroがあるようなので、論文中のdefine-syntaxで定義されたshiftresetマクロは使わずそのまま関数として使い、asyncだけマクロを定義して上の理想的なコードで書けるようにした。

(define *meta-continuation* '())
(call/cc (lambda (cont) (set! *meta-continuation* cont)))

(define (*abort thunk)
  (let ((v (thunk)))
    (*meta-continuation* v)))

(define (reset thunk)
  (let ((mc *meta-continuation*))
    (call/cc
     (lambda (k)
       (begin
         (set! *meta-continuation*
               (lambda (v)
                 (set! *meta-continuation* mc)
                 (k v)))
         (*abort thunk))))))

(define (shift f)
  (call/cc
   (lambda (k)
     (*abort (lambda ()
               (f (lambda (v)
                    (reset (lambda () (k v))))))))))

(define-macro (async . body)
  `(reset (lambda () ,@body)))

(define (send agent msg)
  (shift (lambda (cont) (agent msg cont))))

;; for BiwaScheme
(define (test-biwa-agent msg cont)
  (add-handler! "body" "click" (lambda () (cont msg))))

(define (test-async)
  (async
   (print "before send")
   (print (send test-biwa-agent "hello"))
   (print "after send"))
  (print "wait"))

(test-async)

BiwaSchemeのサイトにいき、トップのREPLに貼り付けて実行すると、クリックした時にメッセージが出るようになる。結果も次のように、望んだものになっている。

(test-async)
before send
wait
hello
after send
hello
after send

結局単にshift/resetの単純な例、というカンジになった。便利だ。

おわりに

そもそもメッセージパッシングならreceiveで受け取るみたいな考え方なんだっけ?とあやふやだ。

あとこのコードに限るけど、何度もsendするとイベントリスナ追加されまくっていくので、非同期処理でもDOMイベントでのコールバック登録ではやるべきでは無さそう。上と併せて、チャンネルみたいなものを作ってそれにsend&receiveって設計がいいのかな。

まあそこらへんは今度やってみよう。面白かったのでヨシとする。

広告を非表示にする