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

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

私に教えられることなら

Schemeで限定継続を使ってClojureのcore.asyncみたいなことをする

(Biwa)Schemeで限定継続を使って非同期処理を書こうとした日記 - レガシーコード生産ガイドClojureのasyncも似たカンジなのかな、などと言ってたけど、気になって検索して次の文書を見ると全く似てなかった。

なので限定継続の使い方を変えて、こんなカンジかな、というコードを書けるようにした。使用例はBiwaScheme用、DOMイベントで非同期処理を行う。(全体のコードは最後に貼る)

使用例

(define (test-regist)
  (let [(ch (chan))]
    (add-handler! "body" "click" (lambda () (>! ch "click!")))
    ch))

(define (test-async)
  (let [(ch (test-regist))]
    (define (main-loop)
      (go
       (print (<! ch))
       (print "after send")
       (main-loop)))
    (print "before send")
    (main-loop)
    (print "wait")
    (>! ch "hello! hello!")))

(test-async)

実行の結果、以下のように出力される。メインループがちゃんとループとして機能していることを確認できる。

before send
wait
hello! hello!    ; ここでまず受け取っている
after send
click!
after send

実装

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

(define *top-level* '())
(call/cc (lambda (cont) (set! *top-level* cont)))

(define (chan)
  (let [(cont *top-level*)]
    (lambda (msg value)
      (case msg
        ['receive (set! cont value)]
        ['send (let [(c cont)] (set! cont *top-level*) (c value))]))))

(define (<! ch)
  (shift (lambda (cont) (ch 'receive cont))))

(define (>! ch value)
  (ch 'send value))

クロージャではマルチスレッド対応の為か、キューにメッセージを追加してそれを受信しているみたいだけど、今回はそれは省いた。

goは単なるresetでいちいちlambdaを書かなくていいようにしただけ。(resetはマクロではなく関数にしている)

chanでチャンネル関数を生成する。'receive時はそのまま、<!を発行した側の(shiftでの)限定継続を受け取り、保存している。goでのresetとの組み合わせによって、それ以降でかつgoの中に限定された残りの計算を行える。

'send時はその保存した限定継続に>!からのvalueを渡すんだけど、受信側がもうない場合に送信されてもいいように、トップレベルの継続を補足しておいて継続をそれに書き換えておく。これでループを抜けても安全、かな。イベントリスナの解除などは別の話になる。

おわりに

具体的にこのチャンネル的なのを使って、どうcallback hellから脱出していくのかはまだわからない。イベントスパゲッティになりそうな気もするけど、チャンネルオブジェクトを常に補足しつつ使うから処理の流れはわかりやすくなるんだろうか。ここらへんは実際にClojureのcore.asyncを使ったり使われたコードを読んだりして調べていきたい。

それからGaucheでも試してみたいので非同期処理をどうやるのか調べなきゃだ。

全体のコード

;; shift/reset  from  "Final Shift for Call/cc: Direct Implementation of Shift and Reset"
;; -------------------------------------------------------------------------------------------------
(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))))))))))

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

(define *top-level* '())
(call/cc (lambda (cont) (set! *top-level* cont)))

(define (chan)
  (let [(cont *top-level*)]
    (lambda (msg value)
      (case msg
        ['receive (set! cont value)]
        ['send (let [(c cont)] (set! cont *top-level*) (c value))]))))

(define (<! ch)
  (shift (lambda (cont) (ch 'receive cont))))

(define (>! ch value)
  (ch 'send value))

;; channel for BiwaScheme
(define (test-regist)
  (let [(ch (chan))]
    (add-handler! "body" "click" (lambda () (>! ch "click!")))
    ch))

(define (test-async)
  (let [(ch (test-regist))]
    (define (main-loop)
      (go
       (print (<! ch))
       (print "after send")
       (main-loop)))
    (print "before send")
    (main-loop)
    (print "wait")
    (>! ch "hello! hello!")))

(test-async)
広告を非表示にする