リスト

リスト

リストはプログラム上では、スペースで区切られた要素を一対のカッコで囲んだ形で記述し、また表示されます。他のプログラミング言語の知識からは、見かけは配列のように見えます。しかし、リストの長さは伸縮自在で、リストに格納する値にデータタイプの制限はありませんので、数値や文字列を混在して並べることもできますし、リスト自体も別のリストの要素とすることができます。

(1 2 3 4)
(“banana” “apple” “orange”)
(1 12.345 “banana” (“apple” “orange”) T nil)

見かけは単純ですが、内部的には配列とは異なる構造をもったデータで、リスト操作の理解にはその独特の構造を頭に入れておく必要があります。

コンスセル

リストはコンスセルという要素で構成されています。

conscell

コンスセルには、二つの記憶領域が存在します。一つ目が該当のコンスセルが保持する具体的な値を指すポインタです。コンスセルに値そのものが代入されているわけではなく、整数のようなアトムでもポインタで参照している点を留意してください。このため、コンスセルはあらゆるデータタイプを組織化できます。二つ目が次の要素のコンスセルへのポインタです。リストの最後では次の要素を表すポインタの値は nil です。

コンスセルを使って、一般的なリストは下図のような内部構造をもっています。

(“banana” “apple” “orange”)
list of LISP

ドットリスト

次のように、二番目の要素を指す記憶領域は、具体的な値を指すものとすることもできます。

conscell

この場合を【コンソール】などで表示してみると、後ろの要素が「.」ピリオドを挟んで連結されて示されるため、ドットリストと呼ばれます。実際のプログラム上の記述も、要素の間にスペースで区切られたピリオドを挟んだ形とします。

(“banana” . 100)

ドットリストの要素が 2 つしかない場合はドットペアと呼ばれます。リストの最後が一般のリストから異なるもののため、ドットが付くのは最後の要素の前、ただ一か所に限られます。

(1 . 2)
(“banana” . 100)
("banana" "apple" "orage" . 300)

ドットリストを含むリストは、以下のようなコンスセルの構造になっています。

(“banana” “apple” “orange” . 300)
dot list of LISP

ドットリストは連想リストを扱う際に必要になります。

コンスセルの作成

コンスセル操作の最も基本的な関数は cons 関数です。通常他の便利な関数があるため、cons 関数を使用する場合は多くはありませんが、cons 関数は他の便利な関数でできることはすべてできる、基礎となる関数です。

AutoLISP関数
(cons item1 item2)
item1、item2 : アトム、リスト、ドットリスト
コンスセルを作成します。
戻り値 : リスト、またはドットリスト

cons 関数はリストの構成単位の最小要素であるコンスセルを作成します。

item1 引数と item2 引数には、それぞれコンスセルの記憶領域の一つ目と二つ目に格納する要素を指定します。

最も単純な例で、要素が一つのリストの作成は次のようになります。次に続く要素が無いため、二番目の引数は nil を指定します。

_$ (cons "orange" nil) ⏎
("orange")

これを繰り返して、要素を二つ以上もつリストを作成することができます。

_$ (cons "apple" (cons "orange" nil)) ⏎
("apple" "orange")

リストを含むリストの場合は次のようになります。

_$ (cons (cons "apple" (cons "orange" nil)) nil) ⏎
(("apple" "orange"))
_$ (cons "fruit" (cons (cons "apple" (cons "orange" nil)) nil)) ⏎
("fruit" ("apple" "orange"))

上でも見てきたように、二番目の要素がリストの場合はリストの要素の先頭に一番目の要素を加えるという結果になります。

_$ (cons "fruit" '("apple" "orenge"))⏎
("fruit" "apple" "orenge")

リストの最後の要素、つまり二つ目の引数をnil 以外にすると、ドットリストが作成されます。

_$ (cons "apple" "orange")
("apple" . "orange")

一般のリストで、要素が二つあれば二つのコンスセルを消費しているのに対して、ドットリストの場合は一つのコンスセルで済んでいることに留意してください。

cons 関数の動作は、一般のリストに加えてドットリストも含めると複雑に見えますが、単にコンスセルを作成し、一番目の記憶領域に最初の引数を指すポインターを、二番目の記憶領域に二番目の引数を指すポインターを代入していることに他なりません。LISP においてはリストというものを理解することが必須となりますが、cons 関数とコンスセルと、それが連なったリスト構造のイメージをもってください。

LISP の関数の引数はリストといえども値のコピーが渡されますが、cons 関数は異なります。

_$ (setq id '(index)) ⏎
(INDEX)
_$ (setq value '(0 1 2 3)) ⏎
(0 1 2 3)
_$ (setq cell (cons id value)) ⏎
((INDEX) 0 1 2 3)
_$ (eq id (car cell))⏎ ;同一のもので、コピーではない
T
_$ (eq value (cdr cell))⏎ ;同一のもので、コピーではない
T

上の実験から判るとおり、cons 関数が返してきたコンスセルが指すものは、cons 関数に渡したものと「同一」のものです。

AutoLISP関数
(vl-list* item [item]...)
item : アトム、リスト、ドットリスト
複数の引数がある場合は、リストまたはドットリストを作成して返します。引数が一つの場合は、引数をそのまま返します。
戻り値 : アトム、リスト、またはドットリスト

cons 関数とよく似たものとしてvl-list*関数があります。cons 関数との違いは以下のようなものです。

  1. 引数が一つ以上で、三つ以上の引数も受け付ける。
  2. 引数が一つの場合は、コンスセルは作成されずそのままの値を返す。
  3. 引数が二つの場合は、cons 関数と同じ動作をする。
  4. 引数が三つ以上の場合は、引数を構成要素とするリストが作成されるが、最後の要素はドットリストとして格納される。

具体的な使用例は以下のとおりです。

_$ (vl-list* "orenge")⏎
"orenge"
_$ (vl-list* "apple" "orenge")⏎
("apple" . "orenge")
_$ (vl-list* "banana" "apple" "orenge")⏎
("banana" "apple" . "orenge")

引数が二つ以上の場合は、cons 関数に置き換えたものをイメージすると理解しやすくなります。

vl-list* 関数 cons 関数
(vl-list* "apple" "orenge") (cons "apple" "orenge")
(vl-list* "banana" "apple" "orenge") (cons "banana" (cons "apple" "orenge"))

このような関数が用意されているわけですが、正直、この関数を使用するケースは思いつきません。

リストの作成

一般的なリストを作成するには list 関数を使用した方が、簡潔で分かりやすく書けます。ドットリストを扱う必要がある場合以外は、通常は cons 関数ではなくlist 関数を使用します。

AutoLISP関数
(list [item...])
item : アトム、リスト
一つまたは複数の式を受け取り、それらを 1 つのリストに結合します。
戻り値 : リスト

list 関数は、複数の要素を受け取って、それらを一つのリストにまとめます。

最も単純な使用例は以下のとおりで、cons 関数を使うより簡潔に書くことができます。

_$ (list "orange")⏎              ; (cons "orange” nil) と同じ
("orange")
_$ (list "apple" "orange")⏎      ; (cons "apple" (cons "orange" nil)) と同じ
("apple" "orange")

リストにリストを含める場合も、同じように書けます。

_$ (list "fruit" (list "apple" "orange"))⏎
("fruit" ("apple" "orange"))

プログラム内で直接リストを書き表す場合は、quote 関数やその省略表記「‘」(シングルクォーテーション)を使っても同じことができました。

_$ '("apple" "orange") ⏎
("apple" "orange")
_$ '("fruit" ("apple" "orange"))⏎
("fruit" ("apple" "orange"))

quote 関数を使用した方が簡単に書けるように見えますが、場合によっては引数を評価しないという quote 関数の特殊な働きが裏目に出ることがあります。つまり、引数を評価する場合です。

_$ (setq a "apple" b "orange")⏎
"orange"
_$ (list a b) ⏎
("apple" "orange")
_$ '(a b) ⏎
(A B)

上の例の list 関数の場合は、引数 a , b が関数に渡される前に評価されて、それぞれ “apple” や ”orange” に置き換わったうえでリストが作成されます。対して、quote 関数の場合は評価されないので、シンボル名のままリストになります。

上の例は単純でしたが、リストの要素にリストを含むときに間違いが起こりやすくなります。単純に list 関数を quote 関数に置き換えることはできず、注意が必要です。

_$ (setq a "apple" b "orange")⏎
"orange"
_$ (list "fruit" (list a b)) ⏎    ; 元の list 関数を使った記述
("fruit" ("apple" "orange"))
_$ '("fruit" (list a b)) ⏎        ; 外側の list 関数を quote 関数に置き換えた
("fruit" (LIST A B))
_$ (list "fruit" '(a b)) ⏎        ; 内側の list 関数を quote 関数に置き換えた
("fruit" (A B))

静的な要素でリストを記述する場合は quote 関数を使い、評価が必要な要素を含む場合は list 関数を使うということですが、混在して使用する場合は、どこが評価されるのかよく吟味してください。

リストの⻑さ

AutoLISP関数
(length list)
list : リスト
リストの要素数を返します。
戻り値 : 整数

length 関数は、リストの要素数を表す整数を返します。

list 引数にはリストを指定します。しかし、ドットリスト自体は受け付けません。リストの構成要素としてドットリストがある場合は問題ありません。

_$ (length '("apple" "orenge"))⏎
2
_$ (length '("fruit" ("apple" "orenge")))⏎
2
_$ (length '(("apple" . 5) ("orenge" . 10))) ⏎
2

誤って length 関数にアトムやドットリストを渡した場合はエラーが発生します。

AutoLISP関数
(vl-list-length list)
list : リスト、またはドットリスト
リストの要素数を返します。ドットリストの場合は nil を返します。
戻り値 : 整数、または nil

vl-list-length 関数は length 関数と同じ働きをしますが、ドットリストがきてもエラーが発生することなくnil を返します。

_$ (vl-list-length '("apple" "orenge"))⏎
2
_$ (vl-list-length '("fruit" ("apple" "orenge")))⏎
2
_$ (vl-list-length '(("apple" . 5) ("orenge" . 10))) ⏎
2
_$ (vl-list-length '("apple" . "orenge"))⏎
nil

長さを知りたいリストが、リストかドットリストかあらかじめわからない時に有用な関数と思いますが、実際に使用するケースは少ないと思われます。なお、vl-list-length 関数にアトムを渡すとやはりエラーが発生します。

リストの結合

AutoLISP関数
(append [list ...])
list : リスト
任意の数のリストを受け取り、それらを 1 つのリストに結合します。
戻り値 : リスト

append 関数は、複数のリストを受け取り、結合します。

list 引数は、いくつでも任意の数のリストを与えることができます。しかし、アトムやドットリストを与えるとエラーが発生します。引数は必ずリストでなければなりません。

最も単純な使用例は以下のとおりです。アトムは必ずリストの体裁にして、append 関数に渡します。また、引数のリストが一つもない場合は、空のリストを表す nil が返ります。

_$ (append (list "apple") (list "orenge"))⏎
("apple" "orenge")
_$ (append) ⏎
nil

append 関数はリストに要素を追加する目的で使用されますが、複数のリストにばらばらに格納されていたデータを一つのリストにまとめる「平坦化(flatten)」の目的でも使用されます。

_$ (append '(1 2) '(3 4) '(5 6) '(7 8) '(9 10)) ⏎
(1 2 3 4 5 6 7 8 9 10)

実際のケースの「平坦化」の例では、ばらばらのリストを格納したリストを append 関数に渡す場合が多いでしょう。そのため、apply 関数を使用します。

_$ (setq a '((1 2) (3 4) (5 6) (7 8) (9 10))) ⏎
((1 2) (3 4) (5 6) (7 8) (9 10))
_$ (apply 'append a) ⏎
(1 2 3 4 5 6 7 8 9 10)

リストの結合は cons 関数でもでき似たところがあります。引数のコピーについて cons 関数は例外で特別でしたが、append 関数について調べてみると、第一引数のみ返り値の要素と「同一」であることが確認できます。

_$ (setq id '(index)) ⏎
(INDEX)
_$ (setq value '(0 1 2 3)) ⏎
(0 1 2 3)
_$ (setq cell (append (list id) value)) ⏎
((INDEX) 0 1 2 3)
_$ (eq id (car cell)) ⏎ ;同一のもので、コピーではない
T
_$ (eq value (cdr cell)) ⏎ ;コピーされたものである
nil

cons か? append か?

一度にリストを作成する場合においては、cons 関数より list 関数が簡潔に書けました。しかし、プログラムにおいては必要に応じてリストに要素を加えていき最終的に大きなリストを作成する場合がよくあります。

このような場合を、append 関数を使って書くことができます。次は シンボル buffer に要素の集合のリストを保持しているとします。このリストの先頭に新しく要素を加える場合は次のように書けます。

(append (list <新しく加える要素>) buffer)

しかし、このようなケースで append 関数の欠点が露呈する場合があります。buffer が巨大なリストになるにつれてメモリーを大量に消費するようになり、 LISP システムがガーベージコレクションというメモリーの整理を頻繁に行うようになります。システム内部の細かい動作は不明で、リストの後ろに要素を加えるように順番を変更しても、あまり改善されません。

このような場合では、cons 関数が有利になってきます。上の操作と同じことを行う cons 関数の書き方は次のとおりです。

(cons <新しく加える要素> buffer)

いつもこの差を意識して、必ず cons 関数の方を使うべきだとは思いませんが、大きなリストを作成している箇所がボトルネックになっている場合に思い出すと、実行速度が大きく改善される場合があります。