お楽しみプログラム

AutoLisp言語

CADソフトの AutoCad に組み込まれている AutoLisp についての話題です。さしあたって、言語としての側面を取り上げます。


スコープ

変数名や関数名の有効な範囲をスコープといいます。関数の中だけでしか見えないローカル変数とか、プログラマは、このスコープのお陰で随分楽をしています。

Lisp のスコープといえば、動的なものだったと思っていたのですが、現在の標準ともいえる Common Lisp や、Scheme では、静的(文脈的?)スコープを採用しています(標準機能としては、ですね。)。

では、AutoLisp は、どうなっているでしょうか。次の例をご覧ください。

_$ (defun func_a (x) (* x y))
FUNC_A
_$ (defun func_b (y) (func_a 5))
FUNC_B
_$ (setq y 1)
1
_$ (func_b 5)
25

func_a が参照している y は、func_b の引数の y で、地面にある 1 にセットした y ではないことがわかります。これは、典型的な動的スコープです。func_b を実行したとき、その中ではすべての yfunc_b の引数の y を示すようになります。そこでこの中で実行される func_a も参照するのは、func_b の引数の y ということになります。

もう少し、変な例です。

_$ (defun func_a (x) (setq y x))
FUNC_A
_$ (defun func_b (y) (func_a 1) y )
FUNC_B
_$ (setq y 0)
0
_$ (func_b 5)
1

func_a は、引数を y に代入する関数。

func_b を引数 5 で呼び出しているので、この中では、y の最初の値は 5
次に func_a を引数 1 で呼び出したので、y1 になります。 この結果、func_b は 最後に y を返しているのですが、その値が 1 になってしまいました。

つまり、呼び出された関数からは、呼び出した関数の引数、(後で触れる)ローカル変数が見えるし、内容の変更も出来てしまいます。 なんとデンジャーな!。

関数の中で一時的に使われるローカル変数は、関数定義の中で、引数の後ろに / を書き、さらにその後ろに続けて書きます。 このローカル変数のスコープも当然ながら、動的スコープです。

_$ (defun func_a (x) (setq y x))
FUNC_A
_$ (defun func_b (x / y) (setq y x) (func_a 1) y)
FUNC_B
_$ (func_b 5)
1

func_a は前の例と同じ。(func_b 5) で呼び出すと、x5 。次の (setq y x)yx を代入したので、y5 。次の (func_a 1) で、なんと、ローカル変数の y の値が 1 になってしまい、最後の y で返す値は 1

見事に、func_a によって func_b のローカル変数が書き換えられてしまいます。


lambda 式

いろいろな言語で、無名関数(とくに名前をつけないで書ける関数)を lambda 式とか、lambada 関数とか言うのは、Lisp の lambda 式に由来するようです。

AutoLispでは、(lambda (x) (* x x)) のように書きます。これは x * x を実行する関数を表します。さて、問題は使い方です。まず、この関数を実行する例です。

_$ ((lambda (x) (* x x)) 2)
4

リストの先頭に書く関数名の変わりに、lambda 式を書けば、その後ろの引数を使って、その場で実行できます。

次にこれを変数に代入してやると、次のように使えます。

_$ (setq a (lambda (x) (* x x)))
#<USUBR @120dfb7c -lambda->
_$ (a 2)
4

さらに、リストに入れておけば、こんな使い方も。

_$ (setq lis (list (lambda (x) (* x x)) (lambda (x) (* x x x))))
(#<USUBR @120dfbb8 -lambda-> #<USUBR @120dfbcc -lambda->)
_$ ((nth 0 lis) 2)
4
_$ ((nth 1 lis) 2)
8

リスト lis の0番目に x * x を計算する関数、1番目に x * x *x を計算する関数をセット。nth は 指定の番号の要素をリストから取り出す関数なので、(nth 0 lis) は、 x * x を呼び出し、結果が 4 。(nth 1 lis) は、x * x * x を呼び出し、結果が 8 になりました。

関数もただのデータとして扱えるわけです。

関数もただのデータなので、関数の引数に出来ます。

_$ (defun twice (f x) (f (f x)))
TWICE
_$ (twice (lambda (x) (* x 2)) 5)
20
_$ (setq s (lambda (x) (* x 2)))
#<USUBR @10d02258 -lambda->
_$ (twice s 5)
20
_$ (defun ss (x) (* x 2))
SS
_$ (twice ss 5)
20

twice の引数 f は関数を期待しています。最初は、twicelambda 式を直接書いた例。

次に、変数 s に lambda 式をセットして、それを引数にして twice を呼びました。

最後に、普通の関数定義を使って、ss という関数を定義し、これを引数にして呼び出しました。


lambda 式 more

mapcar に代表される、引数に関数を取るシステム組み込み関数の引数として、lambda 式を直接書かず、クォートしたものを書きます(あるいは、function を使って書きます。function については、あとで触れます)。

まずは、一般的な書き方。

_$ (mapcar '(lambda (x) (+ x 1)) '(1 2 3))
(2 3 4)

mapcar は、後ろに書いてあるリストの要素を、引数の関数に与えて、繰返し、戻り値をリストにして返却する関数です。(lambda (x) (+ x 1)) は、x+1 を返すので、個々のリストの要素に1を加えた'(2 3 4) が返却されます。

次は、定義した関数を渡す場合。関数名をクォートして渡します。

_$ (defun myfunc (x) (+ x 1))
MYFUNC
_$ (mapcar 'myfunc '(1 2 3))
(2 3 4)

変数に lamda 式をセットしたときも、変数名をクォートして引数にします。

_$ (setq var (lambda (x) (+ x 1)))
#<USUBR @11a1562c -lambda->
_$ (mapcar 'var '(1 2 3))
(2 3 4)

ならば、変数に lambda 式をセットするときに、あらかじめクォートしておけば、クォートは省けるはず?

_$ (setq var '(lambda (x) (+ x 1)))
(LAMBDA (X) (+ X 1))
_$ (mapcar var '(1 2 3))
(2 3 4)

defun 中の defun

defun (関数定義)は、結局、(setq xxx (lambda ...) ) の略記法のようです。さて、defun の中でも defun が使えます。

_$ (defun myfunc () (defun yourfunc (x) (* x x)))
MYFUNC
_$ yourfunc
nil
_$ (myfunc)
YOURFUNC
_$ (yourfunc 2)
4

myfunc の中で yourfunc を定義していますが、myfunc を実行して初めて、yourfunc が定義されます。

yourfunc をローカル変数にしておくと、せっかく定義しても、外側では見えません。ローカル関数として使用できるので、遮蔽としては便利です。(関数定義のオーバーヘッドが重そうですが…)

_$ (defun myfunc ( / yourfunc) (defun yourfunc (x) (* x x)))
MYFUNC
_$ (setq yourfunc nil)
nil
_$ (myfunc)
YOURFUNC
_$ yourfunc
nil
_$ (yourfunc 2)
; エラー: no function definition: YOURFUNC

関数引数の問題

関数に関数を引数として割り当てることが出来ることがわかりました。 これで、自前で mapcar のような関数を書けるはずなのですが…。ここに立ちはだかる大問題があります。

_$ (defun twice (f a) (f a) (f a))
TWICE
_$ (setq sum 0)
0
_$ (twice (lambda (x) (setq sum (+ sum x))) 2)
4
_$ sum
4

twice は 引数の関数 f を二回呼び出す関数です。この関数に sum に集計する関数を食べさせます。後ろの引数を 2 にしたので、sum は期待通り 4 になりました。

ところが集計先を a に変更すると次のように変なことが起きます。

_$ (setq a 0)
0
_$ (twice (lambda (x) (setq a (+ a x))) 2)
8
_$ a
0

動的スコープなので、lambda が実行するときの a が、twice の引数の a を示すようになってしまい、こんな結果になりました。

ここで問題なのは、twice を定義したときの引数 f とか a はいわゆる仮引数で何を書いてもあってもいいはずで、twice を使う人が、これを意識しなくてはいけないというのは、変な話です。さらに、当然ながら、twice でローカル変数を使っていれば、引数に与える関数は、それも、避けなければいけません。

ということで、自前で mapcar のように振舞う関数は書けるけれど、変なことが起きるかもしれない。使うときに使ってはいけない変数を明示しなければならないという制限があります。

この解決策で安易な例は、引数やローカル変数の名前を工夫して、重複しないようにするというもの。例えば、!$%@@arg とかを引数にせよということです。もう少しスマートに名前をつける規則を決めれば、結構実用的でしょう。
もう少し、綺麗に処理したければ、次のようにします。

引数として与える関数が、外部の変数を呼び出さなければ、つまり、自身の引数かローカル変数だけを扱う場合には、この問題は起きません。従って、この関数の引数に必要な情報を含めれば、汎用性を欠きますが、問題を回避できます。
集計などの処理が必要な場合は、次のような関数の動作をすればよいでしょう。

呼び出される側:
 (defun myfunc (val pass) (+ val pass))
呼び出す側:
 (defun caller (f lis pass)
    (setq pass (f (car lis) pass) )
    (setq pass (f (cdr lis) pass) )
    pass
  )
使い方
(setq result (caller myfunc '(1 2) 0) )

呼び出す側が呼び出すたびに前回の結果を渡してやり、最後に結果を返せば良いわけです。


rplaca / rplacdr がない

AutoLisp には、Lisp の基本関数といわれている、rplacarplacdr がありません。無いわけはないのですが、公開されていないということでしょう。公開しない理由は、おそらく、危ない関数だというのが理由だと思います。

さて、では、なぜ、この二つの関数が危ないかという説明は、少し、回りくどくなります。

なぜ、こんな端折った処理でよいのかというと、リストの箱の繋ぎは壊されないからです。使われなくなった箱の回収処理以外ではリストの繋ぎが壊されません。(使われない箱はどうなってもいいので考慮外ですね)

Lispの関数は、箱をつなぎ変えません。必要な場合は新しい箱を用意してコピーを作ったりして、繋ぎ替えを避けて書かれています。このおかげで、 cdr のように、殆どの関数が非常に高速で動作するし、メモリの消費も抑えられます。

さて、問題の rplacarplacdr はこの繋ぎ替えを行う禁断の関数です。いま例に挙げた、

m → [a]→[b]→[c]
n → [b]→[c]

で、 n の方の[b]をつなぎ替えて、[d]を指すように変更します。すると、 m にある[b]も同時に変わってしまいます。(同じ箱なので)

m → [a]→[b]→[d]
n → [b]→[d]

n を繋ぎ替えてしまうと、m の値も変わってしまいます。

これでは、安心してプログラムを組めないですね。

では、この危ない関数が装備されているかといえば、便利な局面があるからです。たとえば、数万件からなるリストの中の1件を差し替えると、繋ぎ変えが許されなければ、リストの大部分をコピーして該当部分を差し替えた新たなリストを作らなければいけません。これは、処理を書くのも面倒ですし、メモリの消費も非常に多くなります。

ところが、該当する部分だけ繋ぎ替えをしてしまえば、処理は一瞬で済みます。メモリーも殆ど消費しません。
プログラマが十分注意を払うという前提を置けば、十分有用だということです。

さてさて、なぜ、実装されていない関数の話が長く続いているのかといえば、この関数は危ないけれど、大きなデータを扱う場合にはどうしても欲しくなる関数だからです。

これが実装されていないので、大きなデータの取り扱いは、面倒で、メモリを食って、さらに、遅くなってしまいます。

でも、自分が供給する立場でであれば、やはり、アプリケーションにアタッチした lisp では、rplacarrplacdr は提供できないです。危なすぎて。


eq と equal

等しいというのは、結構難しい概念。

AutoLispでは、同じものは eq で成立。評価結果が同じ値の場合は、equal で成立ということになっています。さらに、equalでは実数比較の場合の誤差(ε)を指定できます。

eq で等しいと判断されるものは次の通り。

同じ値の整数。 同じ値の実数。 同じ値の文字列。 同じ場所にあるリスト、オブジェクト。

equal で等しいと判断されるのは、次の通り

eqに加えて、ε誤差範囲内の実数。 結果が同じリスト、リスト構造のすべての値の一致を調べているようです。

また、等しいことの意味が問われる関数がいくつかあります(member,assoc,subst,vl-remove )が、これらはすべて equal の意味で等しいものを扱っているようです。


浮動小数の振る舞い

マニュアルの方は実数についてあっさり書いてありますが、ちょっと調べてみました。

  1. 数値の範囲

    1.797693134862315808e308 と書くと 1.#INF になります。

    1.7976931348623158079990999999999e308 は 1.#INF になりません。(仮数部の後ろにいくら9を加えても同じ)
    この数を rtos で見ると、1.797693134862314E+99 と表示されます。指数部がおかしいのは、たぶん、rtosが指数部2桁の表示しかサポートしていないため。

    小数部の変換誤差が影響しているのでしょうが、1.797693134862314E308ぐらいが最大値っぽいです。

  2. 0で割ると…

    「エラー: 0(ゼロ)で除算しました」でエラー中断します。無限大が返ってくるわけではないようです。

  3. 1.#INF

    例えば、(* 1e300 1e300) の演算をすると、値は、1.#INF となります。これはオーバーフロー値のようです。

    $ (setq inf (* 1e300 1e300))
    1.#INF
    _$ (* inf inf)
    1.#INF
    _$ (* inf 0)
    -1.#IND
    _$ (/ 0 inf)
    0.0
    _$ (/ inf 0)
    ; エラー: 0(ゼロ)で除算しました
    _$ (/ inf inf)
    -1.#IND
    _$ (numberp inf)
    T
    _$ (* inf -1)
    -1.#INF
    ; エラー後にリセット
    _$ (/ inf (* inf -1))
    -1.#IND
    _$ (zerop inf)
    nil
    _$ (minusp inf)
    nil
    _$ (> inf 0)
    T
    _$ (< inf 0)
    nil
    _$ (= inf inf)
    T
    

    1.#INF は正の無限大扱いのようです。これに -1 を乗じると -1.#INF になるので、こちらは、負の無限大。
    数値として判定され、大小判断ができます。 (/ inf inf)と、(* inf 0 ) は、-1.#IND になりますが、これは、不定値を表しているのでしょう。

  4. -1.#IND

    -1.#IND は不定値らしいのですが、どんな、扱いになっているか。試しました。

    _$ (setq ind (* 1e300 1e300 0))
    -1.#IND
    _$ (numberp ind)
    T
    _$ (zerop ind)
    nil
    _$ (minusp ind)
    nil
    _$ (> ind 0)
    nil
    _$ (< ind 0)
    nil
    _$ (eq ind ind)
    T
    _$ (/ ind ind)
    -1.#IND
    _$ (* ind 0)
    -1.#IND
    _$ (+ ind 0)
    -1.#IND
    _$ (setq ind2 (* 1e300 1e300 0))
    -1.#IND
    _$ (eq ind ind2)
    nil
    

    -1.#IND は数値扱いだけれど、どんな比較をしても nil になる。自分自身との大小比較もできない。どんな演算をしても結果は -1.#IND。

    ちょっと扱いにくいです。

メニューへ戻る
アルゴ算法堂へ戻る