
コラム
AutoLISPで業務を効率化する 第五回【リスト操作関数 その2】

第四回では「リスト」の操作関数の一部を解説しました。
AutoLISPで業務を効率化する 第四回【リスト操作関数 その1】
第五回は、前回に続けて「リスト」の操作関数を解説していきます。今回はforeach、apply、mapcar等、前回と比べると少しだけ難度が上がりますが、できる限りわかりやすく解説していきます。
ドットペア
「ドットペア」とは、リストを「.」ドットで区切って表したものです。LISPを記述する上では、少し特殊なリストの表示形式とも言えます。
ドットペアを作る【cons関数】
cons関数は、二つの要素をリストに格納し、ドットペアとして出力します。

引数はアトムでもリストでも構いません。ただし、第2引数にリストが入った場合は、内部の処理としてドットが省略されるため、単に要素を結合したリストが作られた様に見えます。
■例:consの第2引数がアトムの時
(cons 0 1)
→(0 . 1)ドットペア
■例:consの第1引数がリストの時
(cons '(0) 1)
→((0) . 1) ドットペア
■例:consの第2引数がリストの時
(cons 0 '(1))
→(0 1)リスト
連想リストからリストを取得する【assoc関数】
リストの中にリストが入ったものを「連想リスト」と言います。そして、リストが連想リストの構造を持つとき、連想リスト内のリストの先頭(car部)は「キー」として扱う事ができます。文字通り鍵そのもので、このキーを目印に連結リスト内から任意のリストを取り出すという処理が可能です。
連想リストという部屋の中にドッドペアという金庫があり、鍵(キー)で開けるイメージと言えばわかりやすいでしょうか。
assoc関数は、キーを引数に与えることで、連想リストからキーの入ったリストを取得します。

キーを含むリストが複数ある場合は、インデックス番号が小さい方のリストが取得されます。
■例:連想リスト((1 “x” 2 ) (“x” 1 2 ) (“x” 0 0))からキー”x“を含むリストを取り出す
(assoc "x" '((1 "x" 2 ) ("x" 1 2 ) ("x" 0 0)))
→(“x” 1 2)
インデックス番号が小さいリストが取得されますが、0番目の(1 “x” 2 )のcar部にあるのは1です。そのため、キーは1になるため、リスト内に”x”があるとしても無視されます。
図形から定義データを取得する【entget関数】
前回も軽く説明した通り、図形定義データの構造は連想リストです。ただ、特殊な点として、図形定義データの中身にはドットペアでなくてはならない要素があります。
そのため、図形定義データを作ろうとしてlist関数を使うと、要素によってはエラーが起きてしまうので、'(0 . “LINE”)という風に直接ドットペアとして記述するか、cons関数を使いましょう。
また、このドットペアのキーは「DXFグループコード」と呼ばれ、図形の各プロパティに対応しています。DXFグループコードとプロパティの対応についてはAutoCAD公式の「DXF グループ コード(番号順)リファレンス」に記載されております。関数リファレンス同様に、活用しましょう。
entget関数は図形から定義データのリストを取り出すことができます。
(entget (ssname (ssget) 0))
また、図形定義データは連想リストであるため、assoc関数を用いればDXFグループコードを使って対応する定義データリストのドットペアを取り出せます。
(assoc 0 (entget (ssname (ssget) 0)))
リストから図形を作成する【entmake関数】
entmake関数は定義データのリストを与えることで図形を作成します。第三回で使用したTEST02のLISPではこの関数を使用してテキストを作成していました。
定義データとして連想リスト内に最低限必要な情報は図形それぞれ異なりますが、グループコード0番の図形の種類だけは必須です。
|
図形 (値) |
必要なグループコード |
|
POINT (点) |
10: 座標値 |
|
LINE (線分) |
10: 始点, 11: 終点 |
|
CIRCLE (円) |
10: 中心点, 40: 半径 |
|
TEXT (文字) |
10: 挿入点, 40: 文字高さ, 1: テキストの文字列 |
|
ARC (円弧) |
10: 中心点, 40: 半径, 50: 開始角度(rad), 51: 終了角度(rad) |
ポリラインもこの関数で作成する事ができますが、説明がややこしいので例として最低限必要な情報を提示しておきます。
■例:座標(0 0 0) (100 0 0) (100 100 0)を通る頂点数が3のポリラインを作る
(entmake
(list
(cons 0 "LWPOLYLINE") ;0:図形の種類
(cons 100 "AcDbEntity") ;100:サブクラスマーカー
(cons 100 "AcDbPolyline")
(cons 90 3) ;90:頂点数
(cons 10 '(0 0 0)) ;10:頂点座標
(cons 10 '(100 0 0))
(cons 10 '(100 100 0))
) ;list
) ;entmake
※定義データ内の情報のリストは必ずこの順番です。順番を変えると作成されません。
リストから図形を編集する【entmod関数】
entmod関数は、定義データのリストを与えることで既存の図形のプロパティを編集します。こちらも第三回のサンプルコードで使用しました。
■例:既存の円を原点に移動させる
(entmod (append (entget (car (entsel)))(list (cons 10 '(0 0 0)))))
※簡略化のために図形定義データのリストに中心座標のリストを結合させていますが、実際にLISPを作る際は第三回の様に条件分岐を組みましょう。
リストを使用する関数
少し難易度が上がりますが、リストを使う便利な関数を軽く紹介しておきます。
リストの要素それぞれに処理を行う【foreach関数】
foreach関数は、引数のリストの要素それぞれを式に当てはめて処理を実行します。

foreach関数を使うと、上の画像の様に大量の同じ式を書く必要がなくなります。式が見やすくなりますし、とても省エネですね。
難しく感じる方は、ピッチングマシンを想像してください。ピッチングマシンは、籠の中のボールすべてに対して、飛ばすという処理を行っていますね。これを籠をリスト、ボールを要素、処理を式に置き換えればそれがforeach関数です。リストの中の要素全てに対して式を実行するわけです。
余談ですが、VBAには For Eachという非常によく似た機能を持つ構文があるので、LISPのforeach関数もfor each(それぞれに対して)というニュアンスだと思われます。
■例:任意の座標(0 0 0) (100 100 0) (200 200 0)に円を作成する
(foreach x '((0 0 0) (100 100 0) (200 200 0))
(entmake (list '(0 . "CIRCLE") '(100 . "AcDbEntity") '(410 . "Model") '(100 . "AcDbCircle")
(cons 10 x) ;★ここの変数名にリストの中の値が入ります
'(40 . 50)
)))
構文は、これまでの関数と比べて少しだけ複雑で、(foreach 変数名 リスト 変数名を含んだ式)と言った感じです。
変数名には、リストの要素が割り当てられて、式が実行されます。ここで定義した変数名と同じ変数が既にあった場合、この処理内ではforeach関数で定義した関数名が優先されるので注意してください。なので、取り立てて理由がなければ、変数名は既存の変数と被らないようにしましょう。
また、リストの要素が代入された形になりますが、処理が終わるとここで定義した変数は破棄されるので、ローカル変数として定義する必要はありません。
一時的に関数を定義しリストを処理する【apply ・mapcar + lambda関数】
Excelでもおなじみのlambda関数です。この関数はその処理の時のみの一時的な変数と処理を定義します。

defun関数と違い、lambda関数が定義するのはあくまで「変数」と「処理」です。具体的な値は引数にはありませんし、名前も持たないので呼び出すことはできません。
これらに値を与える関数としてapply関数と、mapcar関数があります。
apply関数は、lambda関数が定義した変数と対応するリストを1つ渡し、それらを当てはめて処理を行います。戻り値はその処理によって異なります。

■例:(2-3)×1を計算する(リスト(1 2 3)の計算)
(apply
'(lambda (x y z)(* x (- y z)))
'(1 2 3)
)
→-1
mapcar関数は、lambda関数が定義した変数と対応する複数のリストを渡し、それらを当てはめて処理を行います。戻り値は結果が入ったリストです。

■例: (2-3)×1を計算する(リスト(1)(2)(3)の計算)
(mapcar
'(lambda (x y z)(* x (- y z)))
'(1)'(2)'(3)
)
→(-1)
それぞれ、同じ処理を数値を変えて実行したい時に使うという点は一致していますが、戻り値と処理構造が大きく異なります。
vl系関数
autolispには、vl系と呼ばれる関数群があります。これらは後から追加されたものなので文頭にvl-がついていますが、基本的には他の関数とさして変わりありません。似たような名前でvlax系というものもあるのですが、こちらは非常に特殊な関数で、扱いが難しいのでまた別の機会で解説できたらと考えています。
リストの要素が何番目にあるか調べる【vl-position関数】
vl-position関数は、引数に与えた要素がリストの何番目にあるか調べ、インデックス番号の整数を返します。assoc関数と同様に、リストの左から走査し、一番最初に見つかった位置のインデックス番号を返すため、1つ目以降は感知されないので気をつけましょう。該当がなければ戻り値はnilです。また、第1引数にはリストを与えることができます。
■例:連想リスト((0 “あ”) (1 “い”) (2 “う”) (1 “い”)から(1 “い”)の位置を調べる
(vl-position '(1 "い") '(("い" 1) (0 "あ") (1 "い") (2 "う") (1 "い")))
→2
リストから要素を削除する【vl–remove関数】
vl-remove関数は、リストから要素を削除します。構文は(vl-remove 削除したい要素 リスト)で、削除したい要素に一致した要素は全てリストから削除されます。
■例:リスト(0 1 0 1 2)から0を削除
(vl-remove 0 '(0 1 0 1 2))
→(1 1 2)
また、派生としてvl-remove-if、vl-remove-if-not関数があり、これらは削除するルールを決めることができます。

–ifは条件に該当するものを削除し、-if-notは条件に該当しないものを削除します。先ほどの「削除したい要素」に当たる部分に、Tかnilを返す形でlambda関数を定義しましょう。
■例:リスト(0 1 2 3 4 5 6 7 8 9 10)に対し、5未満の数字という条件を与えた時の処理
(vl-remove-if '(lambda (x) (< x 5)) '(0 1 2 3 4 5 6 7 8 9 10))
→(5 6 7 8 9 10)
(vl-remove-if-not '(lambda (x) (< x 5)) '(0 1 2 3 4 5 6 7 8 9 10))
→(0 1 2 3 4)
リストの中身を任意の順番に並べ替える【vl-sort関数】
vl-sort関数はリストの要素を比較関数にしたがって並び替えます。
リスト内の要素をそれぞれ2組ずつ比較して、結果を元に並び替えます。比較関数の結果がTの際に、比較関数に渡された第1引数が前(リストの左側)になるので、「<」は昇順、「>」は降順です。

ちなみに、これまでとは違い第1引数がリストになります。また、リスト内に重複する要素がある場合は、1つにまとめられるので注意してください。
■例:リスト(1 2 1 4 1 5 1 3 0)を昇順「<」で並び替える
(vl-sort '(1 2 1 4 1 5 1 3 0) '<)
→(0 1 2 3 4 5)
関数を必要とする引数2には、これまでと同様にlambda関数で細かい比較条件を定義することができます。引数が連想リストになっている等、そのまま通常の比較関数に渡すことのできない場合にlambda関数を橋渡しとして使用することが多いです。
■例:連想リスト((1 “いち”) (2 “に”) (3 “さん”) (0 “ゼロ”))を降順「>」に並び替える
(vl-sort
'((1 "いち") (2 "に") (3 "さん") (0 "ゼロ"))
'(lambda (x y) (> (car x) (car y)))
);vl-sort
→((3 “さん”) (2 “に”) (1 “いち”) (0 “ゼロ”))
条件関数は、2つの引数を受け入れるので、lambda関数の変数は2つ必要です。
派生として、vl-sort-i関数があります。こちらは戻り値としてソートされたリストが返るのではなく、ソートした結果のインデックス番号の入ったリストが返ります。

ややこしいですが、戻り値の値は元のリストのインデックス番号です。元の要素にインデックス番号の値がはりついて、同時にソートされるのを想像してください。こちらはvl-sort関数とは違って、値が重複してもまとめられることはありません。
■例:リスト(1 2 1 4 1 5 1 3 0)を昇順「<」で並び替える
(vl-sort-i '(1 2 1 4 1 5 1 3 0) '<)
→(8 6 4 2 0 1 7 3 5)
実は、vl-sort関数は第三回にて解説のために作成したTEST02のLISP式にも組み込まれていました。TEST02は「クリックした所にテキストを配置し、x座標の小さい順に0から数字をつける」という謎の関数でしたが、これはx座標値をvl-sort関数を使って昇順に並び替えることで実装していたわけです。(座標値 . 図形名)というドットペアの形で連想リストを作ることで、偶然同じ座標値が選択されたとしても図形名は絶対に重複しないので、リストの要素が消えてしまうことはありません。
余談ですが、ソートアルゴリズムについて公式から詳しい説明はないものの、Lee Mac氏によるとおそらくクイックソートを使用しているとのことです(ソースはこちら)。あまり使いようのない情報かもしれませんが、知っていると何かいいことがあるかもしれませんね。
まとめ
今回は、前回に引き続きリストを操作する関数について解説しました。「リストを使用する関数」や「vl系関数」で使用されるlambda関数に関しては、これまでの関数とは一味違った印象を受けるため、理解するのに時間がかかるかもしれません。しかし、次回予定しているリスト構造そのものについての解説を経ることで、さまざまな疑問が腑に落ちると思います。
用語まとめ
最後に、今回解説した用語をまとめて締めとさせていただきます。
|
名前 |
内容 |
|
ドットペア |
リストをドットで区切った表記 |
|
連想リスト |
(キー データ)という構造のリストを含むリスト |
|
図形定義データ |
図形の情報が入った連想リスト |
|
DXFグループコード |
図形定義データの中のリストのキーにあたり、プロパティと対応している整数 |
|
vl系関数 |
AutoCAD 2000(1999年)で追加された関数群の一つ |
解説した関数一覧。
|
タイトル:ドットペア |
|||
|
関数名 |
内容 |
構文 |
例文 |
|
cons |
二組の要素をドットペアのcar部cdr部にそれぞれ代入して返す |
(cons 要素1 要素2) |
(cons ‘A ‘B)→(A . B) |
|
assoc |
連想リストからキーを含んだリストを返す |
(assoc キー 連想リスト) |
(assoc 0 ‘((0 1 2) 3))→(0 1 2) |
|
entget |
図形の図形定義データを返す |
(entget 図形名) |
(entget (car (entsel))) |
|
entmake |
図形定義データを使って図形を作成する |
(entmake 図形定義データ) |
(entmake ‘((0 . “POINT”) (10 0 0 0))) |
|
entmod |
図形定義データを使って図形を編集する |
(entmod 図形定義データ) |
(entmod (append (entget (car (entsel)))'((62 . 1)))) |
|
タイトル:リストを使用する関数 |
|||
|
関数名 |
内容 |
構文 |
例文 |
|
foreach |
リストの要素それぞれに対して処理を行う |
(foreach 変数名 リスト 式) |
(foreach x ‘(1 2 3) (princ x))→3 |
|
lambda |
1次的に変数と処理を定義する |
(lambda (変数名) 式) |
(lambda (x) (+ x 10))↓以降このlambda |
|
apply |
リストの要素を関数に与えて処理を行う |
(apply ‘関数名 リスト) |
(apply ‘(lambda~) ‘(1))→11 |
|
mapcar |
リストの要素それぞれを関数に与えて処理を行う |
(mapcar ‘関数名 リスト1 リスト2 …) |
(mapcar ‘(lambda~) ‘(1))→(11) |
|
タイトル:vl系関数 |
|||
|
関数名 |
内容 |
構文 |
例文 |
|
vl-position |
リストの要素が何番目か調べる |
(vl-position 調べたい要素 リスト) |
(foreach x ‘(1 2 3) (princ x))→3 |
|
vl-remove |
リストから要素を排除する |
(vl-remove 排除したい要素 リスト) |
(vl-remove 1 ‘(0 1 1 0))→(0 0) |
|
vl-remove-if(-not) |
条件関数に従ってリストから要素(以外)を排除する |
(vl-remove-if ’条件関数 リスト) |
(vl-remove-if ‘(lambda (x) (< x 5)) ‘(0 1 5))→(5) |
|
vl-sort |
リストを比較関数に従って並び替える |
(vl-sort リスト ‘比較関数 ) |
(vl-sort ‘(1 0 5 0) ‘<) →(0 1 5) |
|
vl-sort-i |
リストを並び替えた結果のインデックス番号をリストで返す |
(vl-sort-i リスト ‘比較関数 ) |
(vl-sort-i ‘(1 0 5 0) ‘<) →(3 1 0 2) |