#:g1: どう書く?org: ローカル変数の一覧を取得 (Common Lisp)

Posted 2018-03-18 13:46:33 GMT

久々にonjoさんのページを眺めていて、どう書く?orgの問題を解いているページに遭遇しました。

かれこれ10年以上前のことですが、そういえばこんな問題あったなあと懐しくなりました。
下記のような問題です。

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

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

Pythonで表現すると、下のコードの???部分を埋めることになります。

>>> def foo(): x = 1 y = "hello" ??? return result

>>> foo() {'y': 'hello', 'x': 1}

なお、どう書く?orgは残念ながらアダルトサイトと化してしまったようなのでリンクはarchive.orgです。

Lisp方言の他の方々の回答は、基本構文に仕込みを入れて、Pythonでいうlocalsみたいなものをサポート、という感じです。
自分は、この問題は解いたことがなかったので、色々考えてみました。

Common Lispにおいてローカル変数とは

お題はPythonが念頭にあるようですが、Lisp-1なPythonならローカルのスコープに漂っているのは変数位のものです。
しかし、Lisp-2の権化たるCommon Lispだと、関数、マクロ、シンボルマクロ、スペシャル変数等々がローカルスコープに存在しうります。

実際的にコードウォーキングして何かをする場合、ブロック、GOタグ等を含めていじること多いので、変数だけ取得できても、そんなに嬉しくない気はしますが、それはさておき解答を考えてみます。

解答その一 (実際的に考える)

実際の所、実行時にスコープ内のローカル変数名の一覧を取得したいことというのは殆どありません。
また、遅くなっても良いのだったら、別に言語のスコープのコンテクストをいじらなくても、連想配列等でエミュレートできそうです。

とりあえず、Common Lispには、実行時に名前で変数をアクセスする仕組みとしてスペシャル変数があるので、そういう時はスペシャル変数を使うのが良いだろうということで、こんな感じに書いてみました。

(defun locals () nil)

(defmacro progx ((&rest binds) &body body) `(progv ',(mapcar #'car binds) (list ,@(mapcar #'cadr binds)) (flet ((locals () (append (list ,@(mapcar (lambda (b) `(cons (quote ,(car b)) ,(cadr b))) binds)) (locals)))) ,@body)))

(defun foo (&aux result)
  (progx ((outer 42))
    (progx ((x 1)
            (y "hello"))
      (setq result (locals))
      result)))

(foo)((x . 1) (y . "hello") (outer . 42))

上記のprogxは、実行時の変数結合を実現するprogvをラップしたもので、ボディ内部のlocalsの実行で指定された変数名が取得できます。

スペシャル変数のアクセスはそこまで遅くないので、もし実際的に必要なことがあれば、こういうものでまかなえるのではないでしょうか。

その二 (定義時に処理する)

onjoさんの解答がこちらの系統ですが、onjoさんはAllegro CLのインタプリタ動作時のみ対応とのことでした。
しかし、実行時に外から飛び込んでくるレキシカル変数名というのは考慮しなくても良さそうなのと、外から飛び込んでくるスペシャル変数名は、上記progxのようなもので別途対応すれば良いだろうということで、定義時(マクロ展開時)に確定した情報を取得できれば良しという方針でシンプルに書いてみました。

#+lispworks
(defun get-local-variables (env)
  (mapcar #'compiler::venv-name (compiler::environment-venv env)))

#+sbcl (defun get-local-variables (env) (mapcar #'car (sb-c::lexenv-vars env)))

#+allegro (defun get-local-variables (env) (let* ((base (sys::augmentable-compilation-environment-base env)) #+(:version= 8) (vartab (sys::augmentable-environment-variable-hashtable base)) #+(:version>= 9) (vartab (sys::ha$h-table-ht (sys::augmentable-environment-variable-hashtable base)))) (loop :for var :being :the :hash-keys :of vartab :when (typep (sys:variable-information var env) '(member :lexical :special)) :collect var)))

(defmacro locals (&environment env) `(list ,@(mapcar (lambda (v) `(list ',v ,v)) (get-local-variables env))))

肝は、ローカル変数の一覧が取れるかどうかなのですが、Clozure CLでは取得の方法が分からず対応していません。
ローカル変数の一覧が取れる処理系であれば同様の方法で取得できるかなと思います。

試してみる

こんな感じになります。

(defun foo (&aux result)
  (let ((outer 42))
    (declare (special outer))
    (flet ((bar ()
             (let ((x 1)
                   (y "hello"))
               (setq result (locals))
               result)))
      (bar))))((y "hello") (x 1) (outer 42) (result nil))

また、変数は宣言してから使うことになるので、Pythonと違ってresultも計上されることになりますし、ローカル変数外側のスコープものも見えます。

ちなみに、Pythonで確認してみたところ、外のブロックのものは取得しない様子。

def foo():
  outer = 42
  def bar():
    x = 1
    y = "hello"
    result = locals()
    return result
  return bar()

{'y': 'hello', 'x': 1}

しかし、一番内側のブロックで変数を使えば見えるようです。
どうも、中途半端なような。

def foo():
  outer = 42
  def bar():
    x = 1
    y = "hello"
    z = outer
    result = locals()
    return result
  return bar()

{'y': 'hello', 'x': 1, 'z': 42, 'outer': 42}

まとめ

どう書くorgのページでも議論されていましたが、処理系がデバッグ情報として変数名を含める際にこういう機能が必要になることが多いと思われますが、ユーザーが触って何か有益な処理をするというのはあまり無さそうです。

なお、ご存知の通り、Common Lispでは対話的なデバッガの機能が充実しているので、ローカルな変数名等は大抵見えたり、変更したりもできます。


HTML generated by 3bmd in LispWorks 7.0.0

comments powered by Disqus