#:g1: 束縛部を外から与えるフォーム

Posted 2020-07-23 17:31:13 GMT

bit誌1975-01月号 「連載LISP入門 (13) Backtrack法とLisp」にはbindqというフォームが出てきます。
(1974年から1975年にかけてのbit誌での後藤英一先生のLisp連載)

HLISP独自のようですが、M式で書くと、

bindq[x;y;form]

のような形式でCommon Lispでいうprogvに似た形式です。 qquoteqですが、formがクォートされるので、Common Lispで実装すると、

(defmacro bindq (vars vals form)
  `(let (,@(mapcar #'list 
                   (eval vars)
                   (eval vals)))
     ,form))

となります。

(let ((x 0))
  (bindq (list 'x 'y)
         (list 'x 42)
    (progn (list x y))))(0 42)

という動作ですが、クォートされた変数名がレキシカル変数を捕むという表記はCommon Lispの作法からすると気持ち悪いかもしれません……、と書いているうちに、HLISPはレキシカルスコープじゃないし、要するにprogvではないかとどんどん思えてきました。

この記事を書き始めたときには、とりあえずレキシカルスコープでprogv的なもの、ということを考えていたのですが……、とりあえず、このまま続けることにします。

スコープを作るフォームの束縛部のデータ

前述bindqや、progvではフォームの束縛部のデータ型はリストでした。
letでも((var val))はリストですが、クォートされていて実行時に生成されるリストではありません。

上述で実装したbindqは実行時に評価されそうな見た目ですが、マクロ展開時にフォームは固定されます。
マクロ展開時までに確定できれば変数でも大丈夫ですが、評価フェイズによってはエラーになったりするので、Common Lispの構文作法としてはあまり良くないでしょう。

(defvar *vars* '(x y))
(defvar *vals* '(0 42))

(bindq *vars* *vals* (list x y))(0 42)

まあでも一つの可能性としては面白いかもしれません。

Plasmaでの束縛部のデータ

リスト以外の束縛部のデータといえば、最近だとClojureが配列を採用していますが、古くは、Plasma(1974)があります。
Plasmaでは、sequenceという配列が[]で表記され、setという集合が{}で表記されていますが、これらが、束縛部で使われます。

(let 
   {[x = 42] [y = 0]}
  ...)

(labels {[fib ≡ (cases (≡> [0] 0) (≡> [1] 1) (≡> [=n] (fib (n - 1) + (fib (n - 2)))))]} ...)

束縛部全体は集合で表記され、変数名と値の対は配列で表記されます。

面白いのが、束縛部を変数として与えることが可能なところで、

[math-definitions = 
    {[factorial ≡ ..]
     [fibonacci ≡ ..]
     [cosine ≡ ..]}]

(labels math-definitions body)

という記述が可能とされています。
上記では、labelsのスコープ内に導入しますが、大域環境に定義するenterという機能もあります。

(enter math-definitions)

Plasmaはレキシカルスコープな筈ですが、この辺り実際レキシカルスコープで実現するのは難しそうな機能です。
実際どういう実装がされていたのかは謎……。

ちなみに、Common Lispで真似るならこんな感じでしょうか。
マクロ展開時までに束縛部のデータが確定していれば機能しますが、そうでない可能性を考えると脆弱な仕組みということが分かります。

(progn
  (flet ((rdset (srm chr)
           (let ((tab (make-hash-table :test #'equal)))
             (dolist (elt (read-delimited-list #\} srm T) tab)
               (if (and (typep elt '(vector T 3))
                        (member (elt elt 1) '(= ≡)))
                   (setf (gethash (elt elt 0) tab)
                         (elt elt 2))
                   (setf (gethash elt tab)
                         T))))))
    (set-macro-character #\{ #'rdset))
  (set-syntax-from-char #\} #\))

(flet ((rdseq (srm chr) (coerce (read-delimited-list #\] srm T) 'vector))) (set-macro-character #\[ #'rdseq)) (set-syntax-from-char #\] #\)))

(defpackage plasma (:use) (:export let labels))

(defmacro plasma:let (binds &body body) (let ((binds (eval binds))) (check-type binds hash-table) `(let ,(loop :for var :being :the :hash-keys :of binds :using (:hash-value val) :collect `(,var ,val)) ,@body)))

(plasma:let {[x = 42] [y = 0]} (list x y)) ===> (let ((x 42) (y 0)) (list x y))

(defvar *binds* {[x = 42] [y = 0]})

(plasma:let *binds* (list x y))(42 0)

まとめ

束縛部を実行時データとして与えるというのは動的すぎるとしても、コンパイル時までに与えるというのは活用できる局面があったりするかもしれません。

実際の所、Common Lispではリード時までに与えるというのはたまにありますが、declare等のコンパイラへの指示等が殆どで、束縛部を後で与えたいということは殆どないとは思いますが。

(defvar *bindspec* '((x 42) (y 0)))

(let #.*bindspec* (list x y))(42 0)

参考


HTML generated by 3bmd in LispWorks Personal Edition 7.1.2

comments powered by Disqus