LISPUSER

LISPMEMOLisp isn't a language, it's a building material. -- Alan Kay

(top)  (memo)  (rss)

ローカル変数の一覧を取得 @ どう書く?.org

どう書く?のお題というと、こないだのメールのエンコードもなかなか厳しかったです。ライブラリが充実してないので。 で、 w-o-f TAB で with-open-file に補完、みたいな様子を見てもらうと 解く過程の Flash ムービー を撮ったのですが、 1分15枚の設定でやったら大失敗…。マウスの動きだけ妙に補完されて遅いし、タイプは荒い…。がっかり。

で、またまた Common Lisp には厳しいお題がでていました。

リフレクション系のお題の続編です。ローカル変数の内容を取得して連想配列(ハッシュ、辞書など)に詰めるコードを書いてください。 Pythonで表現すると、下のコードの???部分を埋めることになります。

>>> def foo():
    x = 1
    y = "hello"
    ???
    return result
 
 >>> foo()
 {'y': 'hello', 'x': 1}

(どうかく.org より転載)

これに挑戦してみます。

しかし、コンパイラからするといわゆるローカル変数は定数である事がわかったり、未使用だったりすると最適化の対象にしやすいため 少なくとも変数名と値の対応を全て覚えているというのは期待できませんが…。

解答A: 処理系依存

処理系は本日リリースされたばかりの AllegroCL 8.1 です。

マニュアルの Debugging の項目を読むと、

6.0 ローカル変数とデバッガ

Allegro CL はコンパイルされたコード中のローカル変数の値を調べたり変更したりする 機能を提供しています。しかし、この機能は非常に空間コストが大きいです(情報を デバッガに提供するたみに、コンパイルされたコード中に追加の情報が必要になるためです)。 したがって、この機能は最適化オプションの debug を 3 に設定した場合にのみ利用可能です (コンパイラのデフォルトの設定は 3 になっています)。なお、ローカル変数名の情報(これはデバッグに非常に有用です) は *load-local-names-info* 変数が非 nil に設定された状態でコンパイルされた fasl ファイルを ロードした場合にのみ利用可能です。

とありますので、 (debug 3) の最適化オプションをつけて試してみましょう。

CL-USER> (defun foo ()
           (declare (optimize (debug 3)))
           (let ((x 1)
                 (y "hello"))
               (break)))

FOO
CL-USER> (foo)
 call to the `break' function.
    [Condition of type SIMPLE-BREAK]
 
 Restarts:
  0: [CONTINUE] return from break.
  1: [ABORT] Return to SLIME's top level.
  2: [ABORT] Abort entirely from this (lisp) process.
 
 Backtrace:
   0: (BREAK)
       Locals:
         EXCL::DATUM = #<Function BREAK>
         EXCL::ARGUMENTS = NIL
         X = 1
         Y = "hello"
         EXCL::LOCAL-0 = NIL
         EXCL::LOCAL-1 = 53541613
         EXCL::LOCAL-2 = "call to the `break' function."
         EXCL::LOCAL-3 = #<SIMPLE-BREAK @ #x228d5192>
         EXCL::LOCAL-4 = #<Closure (:INTERNAL BREAK 0) @ #xcc3eb92>
         EXCL::LOCAL-5 = #<Function (:INTERNAL BREAK 1)>
         EXCL::LOCAL-6 = 0
         EXCL::LOCAL-7 = (#<restart CONTINUE @ #x228d520a>)
         EXCL::LOCAL-8 = NIL
         EXCL::LOCAL-9 = NIL
   1: (LET ((X 1) (Y "hello")) (BREAK))
   2: (FOO)

ふむ、インタプリタで実行すると X = 1, Y = "hello" の情報は残っているようです。 次はコンパイルしてみしょう。 (debug 3) で未使用の変数を追跡できるかどうかです。

 CL-USER> (compile 'foo)
 ; While compiling FOO:
 Warning: Variable Y is never used.
 Warning: Variable X is never used.
 FOO
 T
 NIL
 CL-USER> 

うーん、コンパイラが X, Y を未使用変数として認識してしまっています。この状態でブレークしてみると、

CL-USER> (foo)

call to the `break' function.
   [Condition of type SIMPLE-BREAK]

Restarts:
 0: [CONTINUE] return from break.
 1: [ABORT] Return to SLIME's top level.
 2: [ABORT] Abort entirely from this (lisp) process.

Backtrace:
  0: (BREAK)
      Locals:
        EXCL::DATUM = #<Function FOO>
        EXCL::ARGUMENTS = NIL
        EXCL::LOCAL-0 = NIL
        EXCL::LOCAL-1 = 53541885
        EXCL::LOCAL-2 = "call to the `break' function."
        EXCL::LOCAL-3 = #<SIMPLE-BREAK @ #x2291655a>
        EXCL::LOCAL-4 = #<Closure (:INTERNAL BREAK 0) @ #xcc3efd2>
        EXCL::LOCAL-5 = #<Function (:INTERNAL BREAK 1)>
        EXCL::LOCAL-6 = 0
        EXCL::LOCAL-7 = (#<restart CONTINUE @ #x229165d2>)
        EXCL::LOCAL-8 = NIL
        EXCL::LOCAL-9 = NIL
  1: (FOO)

情報が残ってないですね…。というわけで、対象はインタプリタに限定しましょう。 コンパイル時に未使用変数も残す設定はちょっと探した限りではみつからなかったので (debug 3) は諦めます。インタプリタなら未使用の変数も残るようです。

CL-USER> (defun foo ()
            (let ((x 1)
                  (y "hello"))
               sys::*interpreter-environment*))

FOO
CL-USER> (foo)
#<Augmentable INTERPRETER environment 1 1>

で、インタプリタ環境オブジェクトを SLIME 上で右クリックして inspect して中身を調べます。 環境からシンボルへの束縛を取得する API については、AllegroCL のドキュメントに記載されています。 (sys:variable-information 変数名 環境) がそれだ。これは、束縛の種類、値、宣言、ローカルな束縛かどうか、を多値で返す。

インタプリタ以外で実行すると sys::*interpreter-environment* は nil なので、環境がなかった場合はエラーにしておく。

(defun get-local-variables (env)
  (if (null env)
      (error "インタプリタで実行してください")
      (let* ((base (slot-value env 'sys::base))
             (hash (slot-value base 'sys::variable-hashtable)))
        (loop for key being the hash-key of hash
              for information = (multiple-value-list
                                  (sys:variable-information key env))
              for (type locative) = information
              when (eql type :lexical)
              collect (cons key (car locative))))))
CL-USER> (defun foo ()
            (let ((x 1)
                  (y "hello"))
              (get-local-variables sys::*interpreter-environment*)))

FOO
CL-USER> (foo)
((X . 1) (Y . "hello"))

インタプリタで実行しないとダメな点を除けば、題意を満たせた?

解答B: ANSI CL の範囲で頑張る = 俺 Lisp を作成

別解は rubikitch さんの let を改造する版ですかね。 ここでは、 「ほぼ Common Lisp だけれども let がローカル束縛を連想配列として覚えてくれる俺 Lisp」 を作るという方針でゆきます。

といっても、defpackage して、(:use :cl) するだけですが。で、このままだと let のシンボルが 重なるので let は :shadow で隠しちゃいます。新たに俺 let を定義する。その再 Common Lisp の let は cl:let とパッケージ修飾する事で利用可能です。

ローカル変数は local-variables シンボルに連想リストで束縛される事にしましょう。 この俺 Lisp での let は, cl:let を使って以下のように展開されてほしいです。

(let ((x 10)
      (y "hello"))
   ...)
↓
(cl:let ((x 10)
         (y "hello"))
  (cl:let ((local-variables (list (cons 'x x) (cons 'y y))))
    ...))

これで良さそうですが、ネストしたレキシカルなスコープも事実上ローカル変数として参照できます。

(let ((x 10)
      (y "hello"))
   (let ((z 100))
     ...x, y, z が見える!!... ))

これに対応するには、local-variables を append で繋いでゆく、次のような変換を考えます。

(let ((x 10)
      (y "hello"))
   (let ((z 100))
     ...x, y, z が見える!!... ))
↓
(cl:let ((x 10)
         (y "hello"))
  (cl:let ((local-variables (append (list (cons 'x x) (cons 'y y)) local-variables)))
     (cl-let ((z 100))
        (cl:let ((local-variables (append (list (cons 'z z)) local-variables)))
           ....))))

ここで、当然 local-variables はレキシカルスコープである事に気をつけましょう。つまり、 一番外側の let はグローバルな append の引数として local-variables を取りますが、これは グローバル変数でないといけません。しかし、ここで defvar してしまうとダイナミックスコープな 変数になってしまいます。 ANSI Common Lisp の規格ではレキシカルスコープを持つものとして define-symbol-macro がありますので、これでグローバルな local-variables を nil に設定します。

(defpackage :doukaku-lisp (:use :cl) (:shadow #:let))
(in-package :doukaku-lisp)

(define-symbol-macro local-variables nil)
(defmacro let (binding &body body)
  `(cl:let ,binding
     (cl:let ((local-variables (append
                                (list ,@(mapcar (lambda (e)
                                                  (if (consp e)
                                                     `(cons ',(car e) ,(car e))
                                                     `(cons ',e nil)))
                                                 binding))
                                 local-variables)))
       ,@body)))

(defun test ()
  (let ((x 1)
        (y "hello"))
    local-variables))

できました。といっても Scheme 版で Shiro さんが指摘されているとおり、他に変数を導入するものを すべて実装しないといけないので先は長いですが…。

posted: 2007/08/01 23:44 | permanent link to this entry | Tags: MISC

(top)  (memo)  (rss)