週刊nmosh - DLL化 / メッセージパッシングフレームワーク

アプリケーション組み込み機能のまとめ中。いまままでちゃんと整備していなかったので。。

DLL化

nmoshをWin32/64のDLLとして使えるように整備中。まぁ世間的にはLuaで良いだろというポイントでもあるけど、バイトコードの埋め込み等要求の高い機能は抑えていきたい。
あとはリモートREPLくらいか。。現状の難しい問題として、スレッドを起動するたびにFASLイメージから起動しなおしになる(Closureオブジェクト等殆どのSchemeオブジェクトをVM Thread間で共有できない)点がある。このため、スレッドを生成する度に1秒くらい掛かる(!)。
この辺はもう静的なコンパイラを書かないとどうにもならないかなぁ。。

メッセージパッシングフレームワーク

nmoshのいくつかのライブラリはメッセージパッシング方式で書きたい。つまり、クロージャをメッセージポートと見做して、クロージャに値を渡すことをメッセージ送信と約束する。
クロージャを使うメリットは、いわゆるCLOSのようなオブジェクト指向フレームワークに比べて、Scheme側の拡張をあまり要求せずに常識的な書き方が出来る点。
もちろん、

  (define (event . obj)
    (match obj
           (('activate: out)
            (set! output out)
            (set! cursor 0)
            (redraw))
           (('show-header: opt)
            (set! show-header opt)
            (redraw))
           (('leave: out-current?)
            (leave out-current?))
           (('key: . input)
            (apply key input))))
  event)

のように、match構文と普通のdefineでいくらでもメッセージ(風の引数)を受け取る手続きを作ることはできるが、これだとデバッグ用のフックを入れたりするのに不都合なので何か統一的な構文を設けたいところ。
requirementとしては:

非同期I/O APIを使ったプログラムの良いデバッグ手法(message spyや作成したポートの列挙、queueされているメッセージの参照等)をサポートしたいのが一次的な要求。
イベントポートを抽象化することで、パフォーマンスチューニングのための"仕込み"を統一的な手法でいれられるようにする必要がある。

  • 返すメッセージポートを必要に応じてannotationできる

Nmoshには組み込みのlambdaに対するアノテーションが有るし、Weak hashtablesを使えばそもそも任意のオブジェクトに対してアノテーションを付与できる。(nmoshにはいまのところweak hashtableが無いのでそれは作らないといけないけど)

  • 再帰的なcallbackの構成を良くサポートする

非同期I/O APIを使ったコードでは、この手のdefine〜set!コードがよく出てくる。これはみづらいのでどうにかしたい。

(define (main)
  (define out) ;; ← この手の前方宣言がダサい気がする
  (define (handler str)
    (cond
      ((string? str)
       (out "\n")
       (out str))
      ((not str) ;; Close
       (out #f)
       (exit 0))))
  ;; ↓(define out 〜)にすると前方宣言にする必要は無いが、ここに他の式が書けなくなる
  (set! out (wx-listener-create
              'title: "hogehoge"
              'hint: "Input here" ;; ← 名前付き引数hint:を渡している
              handler))
  'ok)

wx-listener-createは、プロンプトとログ領域を持ったWindowを生成する。このとき、プロンプトに入力されたデータを受け取るためのcallback(↑のhandler)を取り、ログ領域に書き込むためのメッセージポート(= クロージャ)を返す。
このとき、wx-listener-createに渡すコールバックでは、メッセージポート(↑のout)を参照したい。なので、最初に(define out)でoutに未定義値を束縛した状態にして、後からset!している。
まぁ今回のケースは

(define (main)
  (define out
    (wx-listener-create
      'title: "Hogehoge"
      'hint: "Input here"
      (^[str]
        (cond
          ((string? str)
           (out "\n")
           (out str))
          ((not str) ;; Close
           (out #f)
           (exit 0))))))
  'ok)

のように書けるが、handlerとoutを同じ深さに書けない。どうしても同じ深さに書きたいならletrec*を使って、

(define (main)
  (letrec* ((handler (^[str]
                       (cond
                         ((string? str)
                          (out "\n")
                          (out str))
                         ((not str) ;; Close
                          (out #f)
                          (exit 0)))))
            (out (wx-listener-create
                   'title: "Hogehoge"
                   'hint: "Input here"
                   handler)))
    'ok))

のように書ける。
今のところ個人的に多用しているイディオムは:

(define (handle-message out str)
  (cond
    ((string? str)
     (out "\n")
     (out str))
    ((not str) ;; Close
     (out #f)
     (exit 0))))
(define (main)
  (letrec* ((handler (^[str] (handle-message out str)))
            (out (wx-listener-create
                   'title: "Hogehoge"
                   'hint: "Input here"
                   handler)))
    'ok))

のように、メッセージポートを引数に追い出す方式。当初は、非同期I/O APIも全部この方式にしようとしていた。つまり、メッセージポートを直接返すのではなく、コールバックに常に供給する方式にすることも考えたが、さすがにコールバックの引数をあまり増やすのも感心できないので止めた。