式の構成
式を組み合わせて、プログラムを組み立てていきます。カッコの組み方で決まるので、異様に見えますが、基本的な考え方は他の手続き型言語と何ら異なることはありません。このことから、最初、LISP に慣れないうちは手続き型言語と同じつもりで書いてもらって構いません。
次の二つの例は、一行で書くか複数行で書くかの違いだけで、同じことを行っています。
式の中に式を含む、つまりカッコの入れ子構造にすると、一番内側の式から評価され、結果が外側の式の引数として置き換えられ、最終的には一番外側の式が評価されて値が定まります。これも、手続き型言語で関数の呼出しの引数に計算式を書くと、まず計算式が計算され、結果が関数に引数として渡されるのと同じことです。優先順位は、内側優先で、かつ左側優先です。
(setq a (* a 2))
(setq a (- a 1))
式を順番に並べると、並べた順番に上から下に式が評価されます。これは、手続き型言語が命令を順に並べるのと同じことです。
上記の二つの例はどちらもシンボル a に、整数 35 が結果として代入されます。
最後に評価された式の値
さらに複雑な式の組み合わせを考えていく時、LISP においては、「最後に評価された式の値」という考え方が重要です。これが関数の場合の戻り値になったりと、LISP では大きな役割を担います。
先ほどの「(- (* (+ 10 5 3) 2) 1)」は外から見ると「一つの式」としてまとまっています。対して「(setq a (+ 10 5 3)) (setq a (* a 2)) (setq a (- a 1))」 は順に並んだ三つの式ですが、このままでは、さらに複雑な式に発展させたい場合でも式を並べておくしか無く「一つの式」の中に組み込むことはできません。このような式の羅列は手続き型言語では違和感はありませんが、LISP においては「一つの式」の書き方が本来の形です。そして、いつでも「一つの式」のスタイルで済ます事ができれば良いのですが、AutoLISP では、いろいろな理由からいくつかのステップを踏む手続き型のコードを書く必要が出てきます。そこで、複数の式を含んだより大きな式としてまとめて囲んでしまい、「最後に評価された式の値」をまとめた式の結果の値とする、「一つの式」化、一種のモジュール化の手法がとられます。
AutoLISP関数 |
---|
(progn [expr]...) |
expr:式 |
各式を順に評価して最後の式の値を返します。 |
戻り値:最後に評価されたexpr の値 |
progn は複数の式をまとめて、ひとつの式のように見せかける関数です。返り値は、最後の式の値となります。
progn 関数は主に if 関数の中で使用して、C 言語の“{ }”、Pascal の“begin end”といったブロックと同じような役割を果たします。if 関数では、条件ごとに実行される式を書きますが、条件ごとの式は一つに限られるため、複数の式を書きたいときは progn 関数で「一つの式」にまとめる必要があるからです。
(if (null symbol) (progn ;then_expr (princ "シンボルは nil です。") (princ "¥n") ) (progn ;else_expr (princ "シンボルの値は ") (princ symbol) (princ "です。¥n") ) )
上の例では、それぞれの progn 関数の戻り値は「”¥n”」や「"です。¥n"」といったものになり、大した意味をもっていません。しかし、単純な区切りを表すブロックと異なる点は、progn 関数が「最後に評価した式の値」を返すという点です。そのため、いくつかのステップを踏んで初期値を求めてシンボルに代入するなど、そのステップの部分を progn 関数で囲むことによって setq 関数内で使用することもできます。
先ほどの三つの順に並んだ式は、progn 関数で囲むことによって外から見ると「一つの式」にまとめることができます。
(progn
(setq a (+ 10 5 3))
(setq a (* a 2))
(setq a (- a 1))
)
この例の progn 関数の戻り値は、最後に評価した「(setq a (- a 1))」の戻り値、つまりは「(- a 1)」を計算した値です。
最後の「(setq a (- a 1))」の setq を外に追いやり、次のように書いても、結果のシンボル a の値は同じです。しかし、最終結果の代入を外に出したことによって、progn 関数の内部が抽象化されモジュール化されています。最終結果をシンボル b に代入したくなった場合は、一番外側のシンボル a を b と書き換えるだけで、progn 関数の中については考えなくて良いわけです。
(setq a (progn
(setq a (+ 10 5 3))
(setq a (* a 2))
(- a 1)
)
)
次の例は、1 から10 の整数の合計をシンボル a に代入します。
(setq a (progn (setq value 0 i 1 ) (repeat 10 (setq value (+ value i) i (1+ i) ) ) value ) )
このように progn 関数で複数の式をまとめてモジュール化を行うことによって、複数の式を並べて書かなければならないケースがあっても、式の入れ子構造になっているより大きな「一つの式」に統合できます。
progn 関数を用いて、式の羅列を「一つの式」としてモジュール化しました。さらにこれを「関数定義をする関数」defun で書式にのっとって囲みますと新しい関数を定義することができます。そのとき、先ほど囲むのに使った progn 関数は省略しても構わないことになっています。これは「暗黙の progn」と呼ばれます。暗黙の progn は、while 関数や repeat 関数、そして多条件分岐の cond 関数でも見られます。
LISP にとっては、プログラムは結局のところ入れ子になった「一つの式」と言えます。関数はその入れ子の式の部分部分に名前を付けて呼び出しているものです。そして LISP においてブログラムを実行するということは、その「一つの式」を入れ子構造の底まで辿りながら評価して行き、最終的に一番外側の式の評価を得るというイメージです。多くの場合はその評価の過程で起こる副作用、AutoCAD で言えば図を描いたりすることが目的ではあります。また C 言語でプログラムの実行は main 関数の実行であるのと同じような話ではありますが、手続き型言語とは哲学的なレベルでものの見方が異なる LISP などの関数型言語の特徴です。
リスト
リストは一対のカッコ「(」・「)」で囲まれ、半角スペースで区切られた要素の羅列でした。この定義は「広い意味でのリスト」にあたり、式も含まれます。ここではデータとしての「狭い意味でのリスト」について確認します。
リストは、データタイプが異なるものも混在して並列できます。また、リストの中にリストを含めることもできます。
(1 2 3 4 5 6)
(“apple” 1 “banana” 2 “orange” 3)
((“apple” 1) (“banana” 2) (“orange” 3))
(expt exp sqrt abs)
(expt 3 2)
「広い意味でのリスト」は、(expt 3 2)のような式の場合も含みます。ただし、このままではLISPは式と解釈して、最初の要素を関数名と理解して実行しようとします。そのため、プログラムの中でデータとしての「狭い意味でのリスト」としてあつかう場合は quote 関数または’(シングルクォーテーション)を使って明示する必要があります。この原則は、「(“apple” “banana” “orange”)」のような明らかに式とは見えないものでも同様です。クォートを忘れると LISP は「“apple”」関数を呼び出そうとして実行時エラーが発生します。
リストは静的なものとして使用する機会は少数で、プログラムの中で動的に構成することが多いでしょう。そのために list 関数など、多くのリスト操作に関する関数が用意されています。これらについては、項を改めて説明します。
式の評価
LISP にとってプログラムとは?といったことを考えてみます。LISP のプログラムは、書かれた状態で「広い意味でのリスト」です。LISP はプログラムとしてのリストを与えられると、「狭い意味でのリスト」を除いて、それを式として評価を行っていくシステムと言えます。LISP プログラムを書く上で、実行時の式の評価はインタープリタが自動的に行いますので、普段は意識する必要はありませんが、eval 関数を用いることで任意の時に明示的に行うことができます。
AutoLISP関数 |
---|
(eval expr) |
expr:式 |
引数を式として評価した結果を返します。 |
戻り値:expr を評価した値 |
シンプルな使い方は以下のとおりです。
_$ (eval '1) ⏎
1
評価されていない整数1 が、eval 関数で評価されて、1 が結果として表示されます。
引数がシンボルの場合を次に二つ示します。最初の例はシンボル a に 100 を代入しています。
一番目の eval 関数は、シンボル a が eval 関数に渡され評価されます。結果100 が返ります。二番目の eval 関数の場合は、eval 関数が評価される前にシンボル a が評価されて100 に置き換わります。つまり、二番目の eval 関数は、100 という引数を評価して 100 にしているだけです。
_$ (setq a 100) ⏎
100
_$ (eval 'a) ⏎
100
_$ (eval a) ⏎
100
次にもう一段複雑な例として、シンボルにシンボル名が代入されている例を見てみます。次の例は、下図のような関係に代入を行っています。
一番目の最初の eval 関数は、シンボルとしての pointer 変数を受け取り、それを評価して代入されているシンボル名を返しています。二番目の eval 関数の場合は、まず pointer 変数が自動的に評価され代入されているシンボル名 value が引数として eval 関数に渡されます。結果、シンボル value が eval 関数によって評価されて、その値が返ります。
$ (setq value 100)⏎
100
_$ (setq pointer 'value) ⏎
VALUE
_$ (eval 'pointer) ⏎
VALUE
_$ (eval pointer) ⏎
100
eval 関数の本来の使い方は以下のようになります。式とリストは似ていました。評価されていないものがリストでした。このことから、式の意味を持つものを、意図的に評価を行わずに変数に代入することもできました。そして任意のときに式として評価するのが eval 関数です。
_$ (setq f ‘(+ 2 5)) ⏎
(+ 2 5)
_$ f⏎
(+ 2 5)
_$ (eval f) ⏎
7
上では、‘(+ 2 5) により式 (+ 2 5) が評価されることなく変数 f に「狭い意味でのリスト」として代入されています。そして改めて (eval f) で変数 f の評価を行うと、「広い意味でのリスト」の内の式として評価され、結果の 7 が得られています。
説明のために、ここまで式やデータを含めた LISP プログラムを「広い意味でのリスト」、単なるデータを「狭い意味でのリスト」と呼んできました。quote 関数が「広い意味でのリスト」の中からデータ部分の「狭い意味でのリスト」を区別するものでした。反対に eval 関数を使うとデータとしての「狭い意味でのリスト」を「広い意味でのリスト」としても自在に扱うことができます。実際の所、LISP においては「広い意味でのリスト」「狭い意味でのリスト」といった区別は無く、これらを区別していません。以降は、これらを区別することなく、単に「リスト」と、LISP 本来の呼び方をします。
AutoLISP関数 |
---|
(vl-symbol-value 'symbol) |
symbol:シンボル |
シンボルに代入されている値を返します。 |
戻り値:アトム、またはリスト |
vl-symbol-value 関数は、シンボルに代入されている値を返します。関数本来のシンプルな使い方は以下 のとおりです。
_$ (setq a 100) ⏎
100
_$ (vl-symbol-value 'a) ⏎
100
シンボル名を与えるとその値を返すことは結果的にeval 関数のバリエーションになりますが、引数がシンボルに限られるという制約があります。引数であるシンボル名を渡すためにクォートすることを忘れないでください。この制約のため、次のようにクォートを行わなかった場合はエラーになります。変数a が先に評価され、vl-symbol-value 関数は 100 を引数として受け取ることになるからです。
_$ (setq a 100) ⏎
100
_$ (vl-symbol-value a) ⏎
_1$
vl-symbol-value 関数は、引数がシンボルである限り、eval 関数と同じように使えます。次の例はシンボルにシンボルが代入されている例です。最初の vl-symbol-value 関数は、シンボルとしての pointer 変数を受け取り、それを評価して代入されているシンボルを返しています。二番目の vl-symbol-value 関数の場合は、まず pointer 変数が自動的に評価されて代入されているシンボルの value が vl-symbol-value 関数に渡されます。結果、value 変数の値が評価されて、その値が返ります。
$ (setq value "100")⏎
"100"
_$ (setq pointer 'value) ⏎
VALUE
_$ (vl-symbol-value 'pointer) ⏎
VALUE
_$ (vl-symbol-value pointer) ⏎
"100"
しかし、eval 関数の重要な機能であるリストを式として評価することはできません。以下の例のシンボル f は vl-symbol-value 関数に渡される前に評価されてリストになります。そして、引数はシンボルに限られるという制約のため、エラーが発生します。
_$ (setq f '(+ 2 5)) ⏎
(+ 2 5)
_$ f⏎
(+ 2 5)
_$ (vl-symbol-value f) ⏎
_1$
eval 関数があれば、vl-symbol-value 関数でできることはすべてできますが、コンテキストに応じて使い分けてください。