クロージャの活用 - 設定ファイル関数、動的オブジェクト・ローダー

クロージャ (内部状態を保持する関数。SICP 2 章では残念な誤用とされております) を用いて、データに対する手続きを抽象化する例をいくつかお見せします。

オブジェクトの動的ローダー

重いオブジェクトを、必要に応じて読み込んだり開放したりするための関数を作ってみました。

(define dynload
  (opt-lambda (load (post-load void) (unload void))
    (let ((o #f))
      (values (lambda ()
                (unless o
                  (set! o (load))
                  (post-load o))
                o)
              (lambda ()
                (when o
                  (unload o)
                  (set! o #f))
                #f)))))

利用例:

(define search-db
  (receive (compile done)
      (dynload (lambda ()
                 (q:compiler-maker (migemo))))
    (lambda (query)
      (cond (((compile) query) => filter-db)
            (else (done))))))

ここでは、データベースの検索式を Scheme 関数に変換する関数 (q:compiler-maker が返すオブジェクト) が内部状態として保持されているんですが、それに対する直接的な操作は関数によって隠蔽されているわけです。

(compile) を評価するまで migemo オブジェクトが読み込まれないので、プログラムの起動が速くなる効果もあります。

また、このコードからは見えませんが、(done) によって migemo.dll の migemo_close() 関数が呼ばれて migemo オブジェクトがメモリから開放される仕組みです。

設定ファイル・オブジェクト

Lisp 系のソフトウェアでは、設定ファイルを単一の連想リストとして持っておくのが最も便利な方法ではないかと思われます。

さらに、クロージャによって設定内容の変更、保存などの手続きを (OO スタイルで) 付け加えると、もっと便利になります。

(define config (load-conf "/path/to/config"))

; get 'foo value
(config 'foo)

; update 'foo value
((config 'set-foo!) (compute-new-foo))

(config 'save)

実装:

(define (lookup k al)
  (cond ((assq k al) => cdr) (else #f)))

(define (load-conf path)
  (let ((conf #f))
    (define (init) (set! conf (load)))
    (define (load)
      (cond ((path-string? path)
             (with-input-from-file path read))
            ((list? path) path)   ; `path' may be alist
            (else #f)))
    (define (save)
      (and (path-string? path)
           (with-output-to-file path
             (lambda ()
               (printf ";; -*- coding: utf-8; mode: scheme; -*-~%")
               ((dynamic-require '(lib "pretty.ss") 'pretty-print)
                conf))
             'replace)
           'done))
    (define (dispatch m)
      (define op?
        (let ((msg (symbol->string m)))
          (lambda (op)
            (cond ((symbol? op) (eq? m op))
                  ((regexp-match op msg)
                   => (compose string->symbol cadr))
                  (else #f)))))   ; error
      (cond ((op? 'init) (init))
            ((op? 'save) (save))
            ((op? #rx"^set-(.+)!$")
             => (lambda (k)
                  (lambda (v)
                    (cond ((assq k conf) => (cut set-cdr! <> v))
                          (else
                           (set! conf (cons (cons k v) conf)))))))
            ((op? #rx"^push-(.+)!$")
             => (lambda (k)
                  (lambda (v)
                    (cond ((assq k conf)
                           => (lambda (l)
                                (unless (member v (cdr l))
                                  (set-cdr! l (cons v (cdr l))))))
                          (else
                           (set! conf (cons (list k v) conf)))))))
            (else (lookup m conf))))
    (init)
    dispatch))

ご覧のように、load-conf は設定項目のキーについての知識を持っていません。つまり、set-foo! などのメソッドは一切定義していないのです。(こういうのをリフレクションって言うんでしょうか?)

また、ここではしていませんが、クロージャを重ねることによって継承やオブジェクトの合成をすることも可能です。

かなり柔軟な OO プログラミングが関数のみで実現できる、という例でした。


追記:

PLT Scheme の match 構文を使うと、上の dispatch 関数は次のように書き直すことができます:

(define (dispatch m)
  (match (symbol->string m)
    ("init" (init))
    ("save" (save))
    ((regexp #rx"^set-(.+)!$" (list _ m))
     (let ((k (string->symbol m)))
       (lambda (v)
         (cond ((assq k conf) => (cut set-cdr! <> v))
               (else
                (set! conf (cons (cons k v) conf)))))))
    ((regexp #rx"^push-(.+)!$" (list _ m))
     (let ((k (string->symbol m)))
       (lambda (v)
         (cond ((assq k conf)
                => (lambda (l)
                     (unless (member v (cdr l))
                       (set-cdr! l
                                 (cons v (cdr l))))))
               (else
                (set! conf (cons (list k v) conf)))))))
    (else (lookup m conf))))

補助関数 (op?) が要らなくなるのでちょっとだけスッキリしますね。