関数

関数の引数

関数に引数で与えられた値は元の値そのものではなく、コピーされた値であることを確認しておきます。これは他のプログラミング言語でも共通することですが、LISP のリストを引数で扱うなどすると混乱する場合があるからです。

コードの説明は省略しますが、次の関数は、与えられたリストの先頭の要素をnil に置き換える関数です。

(defun setNILatHead (alist /)
(cons nil (cdr alist))
)

実行すると以下のようになります。

_$ (setNILatHead '(1 2 3)) ⏎
(nil 2 3)

さて、次のようなコードを実行すると、シンボルa の値はどうなっているでしょうか。

_$ (setq a '(1 2 3)) ⏎
(1 2 3)
_$ (setNILatHead a) ⏎
(nil 2 3)

答えは以下のとおり、元の値のままです。

_$ a⏎
(1 2 3)

関数内での値の書き換えは、元のシンボル a の内容には何ら影響を与えず、コピーされた値に対して行われています。

シンボル a の値を更新したかった場合は、次のように実行するべきでした。戻り値を a に代入するという部分 setq 関数を付け加えます。

_$ (setq a (setNILatHead a)) ⏎
(nil 2 3)
_$ a⏎
(nil 2 3)

この動作は、人によっては当たり前の話と感じられるでしょう。しかしながら、関数の引数は、リストといえども値のコピーである点を強調しておきます。どのような巨大なリストでもコピーとなります。

元の値を書き換える引数の渡し方

通常、呼び出された関数側で引数の元の値を書き換えることはできません。しかし、関数型言語の教義に反することになりますが、抜け道がないか考察します。ここでは、引数に値ではなく、値を保持するシンボル名を与えます。さきほどの与えられたリストの先頭の要素を nil に置き換える関数を、シンボルで受け取る関数に書き換えると以下のようになります。

(defun setNILatHead2 (symbol /)
  (set symbol (cons nil (cdr (vl-symbol-value symbol))))
)

関数内では、(vl-symbol-value symbol) でシンボルの内容を得ます。ここは eval 関数でも構いません。また、(set symbol...でシンボルの内容を書き換えます。setq ではなく、set 関数を使っていることを注意してください。

この関数を次のように実行すると、呼び出し元のシンボル a の値が変更されます。

_$ (setq a '(1 2 3)) ⏎
(1 2 3)
_$ (setNILatHead2 'a) ⏎
(nil 2 3)
_$ a⏎
(nil 2 3)

この方法は、他の言語でポインタを引数で渡す方法に似ています。異なる点は、扱うポインタが増減しても、配列などに入れてインデックスで管理すればよいところを、シンボル名はユニークな名前ですので、増減するデータを配列の代わりにリストに格納するとしても、格納するシンボルに対してどういうユニークな名前を付けるかを考えなければなりません。

そこで、シンボル名の生成をプログラムで管理し、生成されたシンボルをハンドルとして適当な名前の変数やリストに代入して扱うことを検討します。次のサポート関数は、受け取ったデータを、自動的に生成したシンボル名に代入して、そのシンボル名を返します。適当なシンボル名と言っても、既に値が代入されているかどうか、つまり nil でないかどうかはチェックしています。

(setq *handleCount* 0)

(defun getHandle (data / handle)
  (while (boundp (setq handle (read
                                (strcat ":HND-" (itoa *handleCount*))
                              )
                 )
         )
    (setq *handleCount* (1+ *handleCount*))
  )
  (set handle data)
  handle
)

上の関数の使い方は以下のとおりです。「:HND-0」が生成されたシンボル名です。返ってきたシンボル名は、ハンドルとして扱います。

_$ (setq a (getHandle '(1 2 3))) ⏎
:HND-0

この自動生成したハンドル名を適当なシンボルに格納しておき、先ほどのリストを直接書き換える関数に渡します。

a → :HND-01 → (1 2 3)
_$ (setq a (getHandle '(1 2 3))) ⏎
:HND-0
_$ (setNILatHead2 a) ⏎
(nil 2 3)
_$ a⏎
:HND-0
_$ (vl-symbol-value a) ⏎
(nil 2 3)

すべてのデータをこのようにハンドルで管理する必要はまったくありません。しかし、データに複雑に構成されたオブジェクトの振る舞いを持たせたいときには、必要になってくるアイデアです。この仕組みを利用して、スタックオブジェクトを作ってみます。スタックはリストで管理しますが、スタックのデータであることを担保するために、リストの第1 要素はシンボルの STACK とします。

(defun newStack (/)
  (getHandle '(stack))
)

(defun push (stack data / contents)
  (setq contents (vl-symbol-value stack))
  (if (= (car contents) 'STACK)
    (set stack (cons (car contents) (cons data (cdr contents))))
    (exit)
  )
  data
)

(defun pop (stack / contents data)
  (setq contents (vl-symbol-value stack))
  (if (= (car contents) 'STACK)
    (progn (setq data (cadr contents))
           (set stack (cons (car contents) (cddr contents))))
    (exit)
  )
  data
)

スタックオブジェクトの使用例は以下のとおりです。

_$ (setq st (newStack)) ⏎
:HND-1
_$ (push st 100) ⏎
100
_$ (push st "AutoCAD")⏎
"AutoCAD"
_$ (push st 200) ⏎
200
_$ (pop st) ⏎
200
_$ (pop st) ⏎
"AutoCAD"
_$ (pop st) ⏎
100
_$ (pop st) ⏎
nil

匿名の関数の定義

初めて「匿名の関数」と聞くと他の言語の知識ではよく図れないものですが、「名前を付けるほどでもない一時的な手続きの宣言」と言えばよいでしょうか。例えばリストを一括処理する際に、処理する関数を指定するところに直接関数の内容を書いて、オーバーヘッドの低減と可読性を増すことができます。書かれた場所で関数の内容が特定できるので名前をつける必要がなく、名前が無いので「匿名の関数」と呼ばれているようです。「匿名」というと故あって隠れているようなイメージがありますが、「名前が付けられていない関数」という意味です。このような関数は、引数に関数を受け付ける高階関数などで多く用いられます。

AutoLISP関数
(lambda (arguments [/ variables...]) expr...)
arguments:シンボル
variables:シンボル
expr:式
匿名の関数を定義します。
戻り値:関数

匿名の関数定義を行うのが lambda 関数です。lambda は「ラムダ」と読みます。defun 関数に似ていますが、関数名を指定する引数が欠落しています。

ラムダ

関数型言語になじみがない場合は、「ラムダ」というが唐突に聞こえますが、歴史的にラムダは関数を記述する記号の一部からきています。ある数学の分野では関数を表す記号の一部に「^」(キャレット)が使われていました。それが、印刷の関係で字面が似た「Λ」(ラムダ)に変わったそうです。そのため、ラムダという言葉に特別な意味や思い入れが込められているわけではありません。

しかしながら、匿名の関数を表す別名としてラムダ関数やラムダ式という言い方が、今では関数型言語のみならず、その考えを取り入れた手続き型言語でも広く使われて親しまれています。

数値を受け取って正負を反転させる匿名の関数を定義してみます。関数そのものが戻り値となります。

_$ (lambda (a) (* a -1))
#<USUBR @000000002a17a980 -lambda->

匿名の関数の使用は、リストを処理する際などに、mapcar 関数といった高階関数などと組み合わせて使用されます。高階関数については項を改めて説明します。

_$ (mapcar '(lambda (a) (* a -1)) '( 1 2 -1 -2))⏎
(-1 -2 1 2)

なお、mapcar 関数に渡す関数は quote 関数でクォートされている必要がありますが、匿名の関数については quote 関数と同じ働きをする関数として function 関数が存在します。

AutoLISP関数
(function symbol | lambda-expr)
symbol:シンボル
lambda-expr:匿名の関数
与えられたシンボルやラムダ式を、AutoLISP のコンパイラが関数としてコンパイルするように指示します。
戻り値:シンボル、または、クォートされた匿名の関数

quote 関数との違いは、匿名の関数を function 関数でクォートすると、AutoLISP はクォートされたリストとしてではなく関数としてあらかじめ評価を行います。機能上、アルゴリズム上の違いはなく、最適化のための使い分けです。

使用例として、先ほどのものと同じ事をする場合は以下のようになります。quote 関数の代わりに function 関数で囲みます。

_$ (mapcar (function (lambda (a) (* a -1))) '( 1 2 -1 -2))⏎
(-1 -2 1 2)

function 関数の最適化を実際に目で確認することができます。下の実行例は quote 関数は原則通り式のリストをリストのままクォートします。しかしながら fucntion 関数は内部の lamda 関数を予め評価して関数としたものをクォートします。なお、function 関数が直接囲めるのは lamda 関数のみで、後述の関数を返す関数ではエラーになります。

_$ '(lambda (a) (* a -1))
(LAMBDA (A) (* A -1))
_$ (function (lambda (a) (* a -1)))
(quote #<USUBR @000001b20030cb60 -lambda->)

匿名の関数をシンボルに代入することによって、いわば名前を付けたことになり、通常の関数のように扱えます。使用するケースは少ないかもしれませんが、匿名の関数をローカル変数に代入することによって、ある範囲からしか実行できないローカル関数として使用することができます。以下の例では foo 関数を実行すると内部の mysqr というローカル関数が働いて10 の二乗の100 が返ります。

(defun foo (/ mySqr)
  (setq mySqr (lambda (x) (* x x)))
  (mySqr 10)
)

上と同じことを defun 関数で以下のように書くことができます。

(defun foo (/ mySqr)
  (defun mySqr (x) (* x x))
  (mySqr 10)
)

このことはつまり、defun 関数は、lambda 関数で関数を定義し、指示された関数名のシンボルに代入するということと同じことを行っていることを示しています。

lambda 関数の戻り値は関数そのものです。ですから、lambda 関数の戻り値をカッコで囲むことによって、その場で実行することができます。関数に引数を渡したければ、同じカッコの中に囲みます。

_$ ((lambda (a b /) (+ a b)) 1 2) ⏎
3

上の例は a と b とを引数として持つ匿名の関数を定義し、a に対して 1 を、b に対して 2 を引数として与えて実行しています。

通常の関数と匿名の関数の違い

匿名の関数をシンボルに代入することで、そのシンボル名を関数名として他の通常の関数と同じように使えます。プログラムを書いていて、その違いは意識する必要はありません。

しかし、現在、両者の関数の扱いが異なることが一点だけ判っています。【独自の名前空間 VLX アプリケーション】を構築する際、外部に関数をエクスポートする vl-doc-export 関数は、defun で定義した関数名しか受け付けません。匿名の関数を代入したシンボル名は指定できません。