次へ 前へ 目次へ 佐々木将人の個人ページへ

2 Lispの内部構造

 竹内郁雄先生の「はじめての人のためのLISP」では,Lispの御本尊として紹介されている話です。
 Lispにおいては,プログラムとデータの区別はありません。どちらも同じ形式で内部で保存されます(もしくは同じ形式で保存されているかのようにふるまいます)。この単純さが,「何か一連の文字の塊を1個読み込み、「評価」と言って一定の規則に基づいて処理をし、その結果を返します。その繰り返しです。」がLispの基本であると説明できてしまうことにつながります。その単純さを具体的に見ていきます。

 まず記憶領域を2つ組み合わせたものを1つの単位として扱います。これをcons cellもしくは単にcellと呼んでいます。
 「何か一連の文字の塊」が ( で始まると,read関数は,このcons cellを用意します。
 一般的なLispの本では,cons cellを次のように図示するのが通例です。

 ┌─┬─┐ 
 │ │ │
 └─┴─┘
 ( の次に文字アトム a が来たとしましょう。
 アトムなので a 自体は,アトムの専用領域に収納され(そしてある種のデータベースとなって,同じアトムは複数存在しないよう仕掛ける)そこへのポインターがcons cellの前半部に収納されます。
 これを次のように図示することにします。
 ┌─┬─┐
 │・│ │
 └│┴─┘
  ↓
  a 
 a の次に ) が来たとします。
 上のcons cellに入れるべきものがない……ということでたいていのLispの本では斜線を引きます。
 (なお,ここではできるだけASCII文字で表現するという方針で斜線を引くのではなく,斜線を示す文字を入れています。)
 ┌─┬─┐
 │・│\│
 └│┴─┘
  ↓
  a 
 ということで (a) は,内部ではこういうように格納されているのでした。

 ちなみに,内部構造を示すのに,この図を書かなければならないとなると,場所も取りますし,結構大変です。
 そこで,この内部構造に忠実な表現というのも別途存在します。
 それが
 (a . NIL)
 です。(NILはnilでもかまいません。)
 一般的には ( 前半部 . 後半部 ) という形式です。( の後や ) の前の空白は必須ではありません(=読み飛ばされる)が,前半部と後半部を区切る . (ピリオド)の前後には空白が必須です。空白がないと意味が変わってしまいます。
 ちなみにこの表現を「ドット対(dotted pair)」による表現と呼んでいます。

 別の例です。
 「何か一連の文字の塊」が ( で始まると,read関数は,このcons cellを用意するのですが,
 ┌─┬─┐ 
 │ │ │
 └─┴─┘
 その次も ( だと,リストということですから,cons cellをもう1つ用意し,最初のcons cellの前半部にもう1つのcons cellへのポインターを収納します。
 これはこのような図になります。
 ┌─┬─┐
 │・│ │
 └│┴─┘
  ↓
 ┌─┬─┐
 │ │ │
 └─┴─┘
   ( 2つの後に a が続くと
 ┌─┬─┐
 │・│ │
 └│┴─┘
  ↓
 ┌─┬─┐
 │・│ │
 └│┴─┘
  ↓
  a
  その次に ) が来ると,a が前半部に入っているcons cellの後半部にNILを入れ,
 ┌─┬─┐
 │・│ │
 └│┴─┘
  ↓
 ┌─┬─┐
 │・│\│
 └│┴─┘
  ↓
  a
  さらに ) が続くと,最初のcons cellの後半部にNILを入れることになります。
 ┌─┬─┐
 │・│\│
 └│┴─┘
  ↓
 ┌─┬─┐
 │・│\│
 └│┴─┘
  ↓
  a
 ドット対による表現だと ((a . NIL) . NIL) となりますが,一般的な表現だと ((a)) になります。
(なお,ドット対による表現では . の前後の空白は省略できないとしましたが,( や ) などに続く場合には省略可能です。より正確には「区切り文字」と呼ばれる文字についてはその一文字で何らかの意味を持つため,前後に . がなくても意味が変わらないため省略可能です。)

 もう一つ別の例。
 ( a と続いて,その次に b が来た場合。
 ┌─┬─┐
 │・│ │
 └│┴─┘
  ↓
  a 
 このcons cellの後半部には b へのポインターは収納できません。というのは,リストというのは長さが固定されていないので,b の後にさらにリストやアトムが続いた時に困ってしまうからです。というので,別のcons cellを用意して,そこへのポインターを後半部に収納します。その上で,新しいcons cellの前半部に b を収納します。
 ┌─┬─┐ ┌─┬─┐
 │・│・─→│・│ │
 └│┴─┘ └│┴─┘
  ↓     ↓
  a      b
 ( a b の次に c が来て,さらに ) が続くとこうなります。
 ┌─┬─┐ ┌─┬─┐ ┌─┬─┐
 │・│・─→│・│・─→│・│\│
 └│┴─┘ └│┴─┘ └│┴─┘
  ↓     ↓     ↓
  a      b      c
 ドット対による表現だと,(a .(b .(c . NIL))) となりますが,一般的な表現では (a b c) です。

 繰り返しになりますが,Lispではプログラムとデータの区別をせず,このような形式で統一的に扱い,「何か一連の文字の塊を1個読み込み、「評価」と言って一定の規則に基づいて処理をし、その結果を返します。その繰り返しです」という単純な原理でコンピューターを動作させることを可能としているのです。

 なお,Lispの内部構造を記述できるドット対ですが,まれに,内部構造を記述しきれない場合があります。その典型例が
  ┌─────────────┐
  ↓             │
 ┌─┬─┐ ┌─┬─┐ ┌─┬│┐
 │・│・─→│・│・─→│・│・│
 └│┴─┘ └│┴─┘ └│┴─┘
  ↓     ↓     ↓
  a      b      c
のように,ループを形成するものです。通常はこういうことにはならないのですが,Lispではこの内部構造に手を入れることも可能になっているので,作業次第ではこういう内部構造も作られてしまいます。そしてこれをprintすると,(a b c a b c a ……) と止まらなくなってしまいます。(処理系によっては,こういうループを感知して停止させてしまうものもありますが。)このパターンはドット対でも内部構造を正確には表現できません。

cons cellの前半部を返す関数……car

 引数にとったリストのcons cellの前半部が何かを返す関数です。返す値はリストの場合もありますし,アトムの場合もあります。
 (a . (b . (c . NIL))) であれば,a を,((a . NIL) . (b . (c . NIL))) であれば,(a) を返します。
 引数にリスト以外を渡した時の扱いは定まっておらず,処理系によってエラーを返したり,何も返さなかったり,適当な値を返したりします。

lfe> (car '(a b c))
a
lfe> (car '((a) b c))
(a)

※ちなみにLFEでリスト以外を引数に渡すと,引数が適切でないという例外を返しますが,デバッグモードには入りません。

cons cellの後半部を返す関数……cdr

 引数にとったリストのcons cellの後半部が何かを返す関数です。返す値はたいていはリストですが,アトムの場合もあります。
 (a . (b . (c . NIL))) であれば,(b c) を返します。ドット対については,入力を受け付けるものの,出力としては,簡略化できるものは簡略化し,どうしても簡略化できない(cons cellの後半部にNIL以外のアトムを指すポインターが収納されている場合がその典型的な例です)時にだけ。ドット対で出力するのが通例です。簡略化できる場合というのは,例えば (b . (c . NIL)) のように後半部がリストで,かつそのリストの前半部がアトムだと, . とそれに引き続く ( と ) の組合せを外してしまい (b c . NIL) とします。さらに . の後がNILだと,リストの終端を示しているので,. NILを外して,(b c) とします。
 引数にリスト以外を渡した時の扱いは定まっておらず,処理系によってエラーを返したり,何も返さなかったり,適当な値を返したりします。

lfe> (cdr '(a b c))
(b c)

※ちなみにLFEでリスト以外を引数に渡すと,引数が適切でないという例外を返しますが,デバッグモードには入りません。

carとcdrの組合せ

 例えばリストの1番目の要素を取り出すには (car リスト) でいい訳ですが,2番目の要素を取り出す時にはリストのcdrを取って,その結果のcarを取るという手順になります。Lispで書くと (car (cdr リスト)) になります。ところで,このように「何番目の要素を取り出す」というのは結構需要があるので,多くの処理系では複数のcarやcdrを組み合わせてcxxxrの形式で一つの関数として用意していることがよくあります。(例えば (car (cdr リスト)) だと,(cadr リスト) の要領です。)どんな(どれだけ)関数があるかは,その処理系のマニュアル等を見てください。
 例えばLFEではcxxxxrのように間のx(aかd)が4つまで定義されており,5つ以上だと未定義の関数だと返されます。

carとcdrの逆に新たなリストを作る……cons

 新たなcons cellを持って来てリストを作る関数がconsです。consは必ず引数を2つ持ちます。それぞれの引数はリストでもアトムでもかまいません。(ただし第2引数はリストである場合が多いでしょう。アトムだと,ドット対でしか表現できないデータが出来上がってしまうからです。)
 ちなみに,consがcarとcdrの逆だという話なのですが……。
 一言で言ってしまうと,(cons (car x) (cdr x)) が,元のxというリストと同じになるからなのです。すなわちconsは,新たなcons cellを1つ持って来て,その前半部に第1の引数,後半部に第2の引数を収納するのですが,(car x) は元のxの最初の要素,(cdr x) は最初の要素を取り除いた残りのリストとなるため,それをconsすると元のリストに戻るという仕掛けなのです。

lfe> (cons 'a '(b c))
(a b c)

他のLispにおけるcar・cdr・cons

 Common lisp,Scheme,Emacs Lispでも,これらの基本関数については存在し,返す値も変わりません。
 carとcdrの組合せのcxxxxrの形式の関数については,これは言語というより,個々の処理系で扱いが異なっています。

(2023.5.14. 初版)
(2023.5.15. 改訂)

次へ 前へ 目次へ 佐々木将人の個人ページへ