M.Hiroi's Home Page

xyzzy Lisp Programming

Common Lisp 入門

[ PrevPage | xyzzy Lisp | NextPage ]

リストの操作

●リストの分解

最初はリストの分解から始めましょう。関数 car はリストの第 1 要素を取り出し、関数 cdr はリストの第 1 要素を取り除いたリストを返します。なお Common Lisp の場合、car と cdr ではなく firstrest を使うことが推奨されています。ですが、M.Hiroi は古い人間のため car と cdr を好んで使っています。皆さんは first と rest を使ってくださいね。


                  図 1 : car と cdr の操作

car と cdr は関数ですので、リストには引用符を付けることを忘れないでください。また、リスト以外のデータを与えるとエラーとなります。

car と cdr はリストを分解します。上図に示すように、car は先頭のセルの CAR 部に格納されたデータを返します。cdr は CDR 部に格納されたデータ (後ろに接続されているセル) を返します。つまり、先頭要素を除いたリストを返すことになります。

要素が 1 つしかないリストに cdr を適用すれば、空リスト nil を返します。また、car も cdr も引数に nil が与えられると nil を返します。

(cdr '(a)) => nil
(car nil)  => nil
(cdr nil)  => nil

今度は要素がリストの場合を考えてみましょう。次の例を見てください。

(car '((a b) (c d) (e f))) => (a b)
(cdr '((a b) (c d) (e f))) => ((c d) (e f))

リストの第 1 要素は (a b) であり、a ではないことに注意してください。a を取り出したい場合は次のようになります。

(car (car '((a b) (c d) (e f)))) => a

それでは、第 2 要素を取り出す場合はどうするのでしょう。car と cdr を組み合わせれば簡単に取り出すことができます。

(car (cdr '(a b c d e f)))
     ~~~~~~~~~~~~~~~~~~~~ が評価されると a が取り除かれる。
   ||
(car '(b c d e f))  取り除いた後のリストの第 1 要素を取り出す。
=> b

このように、car と cdr を組み合わせることで、リストのどの要素にもアクセスすることができるのです。また、car と cdr を組み合わせた関数 cxxr, cxxxr, cxxxxr も用意されています。x には a か d が入ります。次の例を見てください。

(cadr '(a b c d)) ≡ (car (cdr '(a b c d))) => b
(caddr '(a b c d)) ≡ (car (cdr (cdr '(a b c d)))) => c
(cadddr '(a b c d)) ≡ (car (cdr (cdr (cdr '(a b c d))))) => d

昔の Lisp では、リストの第 2 要素や第 3 要素を取り出すために cadr や caddr がよく使われました。これらの関数は Common Lisp にも用意されていますが、第 2 要素は second で、第 3 要素は third で取り出した方がよいでしょう。このほかに、第 4 要素を取り出す fourth から第 10 要素を取り出す tenth まで用意されています。

●リストの合成

次は、リストの合成です。関数 cons はリストの先頭にデータを付け加えます。


                          図 2 : cons の動作

car と cdr で分解したリストは cons で合成することができます。


                  図 3 : リストの分解と合成

この関係は、リストを操作する関数を作る場合の基本です。とても重要な関係なので、覚えておいてください。

関数 list は、その引数を要素とする新しいリストを返します。


                      図 4 : list の動作

上図のように、list は引数を新しいリストの要素として格納します。引数がリストの場合は、それが要素となります。リスト同士をつなぐのではないことに注意してください。

これに対し、関数 append はリスト同士を接続します。したがって、引数はリストでなければいけません。アトムを与えるとエラーになります。


                  図 5 : append の動作

append はリストを接続する場合、新しいセルを作成して引数の要素を格納します。ただし、いちばん最後の引数は新しいセルを作成しません。要素自体には何もしないことに注意してください。

(append '((a b) (c d)) '(e f))
=> ((a b) (c d) e f)

(append '((a b) (c d)) '((e f)))
=> ((a b) (c d) (e f))         ; (a b c d e f) ではないことに注意

このように、要素がリストの場合でも、そのリストの要素を取り出すことはせずに、引数の要素を並べるだけです。

●リストの操作は非破壊的

今まで紹介した関数はシンボルの値を変更しません。次の例を見てください。

(setq var '(a b c d e))
=> (a b c d e)
(cdr var)
=> (b c d e)
var
=> (a b c d e)

シンボル var の値は破壊されません。var の値を変更したいときは setq や setf を使います。

(setq var (cdr var))
=> (b c d e)
var
=> (b c d e)

以上のように、シンボル var の値は更新されます。

●ドットリスト

ここで、もう一度 cdr に戻ります。cdr は先頭の要素を取り除いたリストを返しましたが、それはセルの CDR 部を返す働きと同じです。最初に、最終セルの CDR にはリストの終わりを示すデータが格納されることを説明しました。したがって、要素がひとつしかないリストに cdr を適用すると、リストの終端を表すデータが取り出されます。もうおわかりだと思いますが、この特別なデータが nil なのです。

今までのリストは、すべて終端に nil がセットされています。ところが、Lisp の立場になって考えてみると、リストの終端は CDR 部に格納されるデータがセル以外であれば、そこがリストの終端であることがわかります。つまり、nil でなくてもかまわないのです。リストの終端が nil 以外のデータである場合、そのリストを次のように表します。

左右の括弧の中間にドット ( . ) を置き、左側に CAR 部のデータを、右側に CDR 部のデータを書きます。つまり、リスト (a) は (a . nil) と表すことができます。このようなデータをドット対 (dotted pair) と呼びます。たとえば、CAR 部がシンボル a で CDR 部がシンボル b であれば (a . b) となります。普通のリストも次のようにドット対を使って表現できます。

(a)           ≡ (a . nil)
(a b c)       ≡ (a . (b . (c . nil)))
((a b) (c d)) ≡ ((a . (b . nil)) . (c . (d . nil))) [訂正] '02-4-15
((a b) (c d)) ≡ ((a . (b . nil)) (c . (d . nil))) [訂正] '02-6-27
              ≡ ((a . (b . nil)) . ((c . (d . nil)) . nil))
((a b) c d)   ≡ ((a . (b . nil)) . (c . (d . nil)))
-- [訂正] '02-4-15, '02-6-27 -----
例題に誤りがありました。訂正するとともにお詫び申し上げます。
参考文献 [2] によると、カッコ ( ) の中間にドットを置いてリストを表すことをドット表現といい、ドットを使わずにリストを表すことをリスト表現というそうです。そして、リスト表現とドット表現を混在させた表記法を改良リスト表現といい、多くの Lisp 処理系がこの表記法を用いています。((a . (b . nil)) (c . (d . nil))) は ((a b) (c d)) と同じリスト構造を表していますが、改良リスト表現になっているためドット表現 ((a . (b . nil)) . ((c . (d . nil)) . nil)) に訂正しました。

それでは、リスト (a b c) の終端を d に変えてみましょう。ドット対を使った表記法では、(a . (b . (c . d))) となりますが、これは (a b c . d) と表すことができます。

このように、nil 以外のアトムで終端されたリストをドットリスト (dotted list) と呼びます。

ドットの後ろは cdr にセットするデータを指定するのですから、複数のデータを書いたり省略してはいけません。次の場合はエラーになります。

( . a)       ; car がない
(a . )       ; cdr がない
(a . b c)    ; cdr にデータが複数ある
(a . . b)    ; ドットが複数ある
(a . b . c )

数と算術演算

Common Lisp では、いくつかの種類の数が定義されています。大きく分けると整数 (integer)、分数 (ratio)、浮動小数点数 (floating-point number)、複素数 (complex number) という 4 種類の数があります。xyzzy Lisp でもこれらの数を使うことができます。

●整数

Common Lisp の場合、整数の大きさには制限がありません。これは xyzzy Lisp も同じで、5000! や 10000! でも求めることができます。ただし、処理系の内部では整数を fixnum と bignum (多倍長整数) に分けています。その処理系で効率良く整数を表せる範囲が fixnum で、それ以外の整数が bignum となります。fixnum の範囲は処理系に依存しますが、その値は変数 most-negative-fixnum と most-positive-fixnum に格納されています。

most-negative-fixnum => -2147483648
most-positive-fixnum =>  2147483647

このように、xyzzy の fixnum は 32 bit で表されています。

整数は通常 10 進表記ですが、それ以外の基数でも書くことができます。

#nnrddddd または #nnRddddd

nn が基数 ( 2 - 32 ) を表します。よく使われる基数には省略形が用意されています。

簡単な例を示しましょう。

#4r1230123 => 6939
#b10101010 => 170
#o1234567  => 342391
#o-127     => -87
#xabcdef   => 11259375

●分数

Common Lisp は分数を扱うことができます。分数は 2 つの整数を / で区切って表します。簡単な例を示します。

1/2, 2/3, 4/3, 11/13, -51/100, 30517578125/32768

また、4/6 や 3/12 のような入力もできますが、この場合は約分されることになります。とくに、4/2 のような割り切れる分数は、ただちに整数に変換されます。次の例を見てください。

4/6  => 2/3
3/12 => 1/4
10/5 => 2    ; 整数に変換される

●浮動小数点数

Common Lisp は浮動小数点数を扱うことができます。浮動小数点数は一種類だけではなく、処理系によって複数の種類を持つことができますが、その精度と大きさは処理系に依存します。Common Lisp で推奨されている浮動小数点数の種類を表 1 に示します。

表 1 : 浮動小数点数の種類
形式最小精度最小の指数の大きさ指数マーカ
小精度 (short-float)13 ビット5 ビットs, S
単精度 (single-float)24 ビット8 ビットf, F
倍精度 (double-float)50 ビット8 ビットd, D
長精度 (long-float)50 ビット8 ビットl, L

浮動小数点の表記法は次のようになります。

[+|-] 数字 小数点 数字 指数マーカ [+|-] 数字
例: 0.1234                   ; single-float
     0.1234d0                 ; double-float
     1.2345e10                ; single-float
     -9.876542999999999d-100  ; double-float

Common Lisp の仕様では、これら 4 つの形式をすべてサポートしなければならないわけではなく、処理系によっては 4 つより少なくてもかまいません。xyzzy Lisp には 2 つの内部形式があり、short-float と single-float が同じ形式、double-float と long-float が同じ形式になります。ようするに、C/C++ の float (単精度浮動小数点型) に相当するのが short-float, single-float で、double (倍精度浮動小数点型) に相当するのが double-float, long-float になります。

なお、指数マーカには e, E を使うことができます。この場合、浮動小数点数は変数 *read-default-float-format* に格納されている形式に変換されます。また、指数マーカを省略した場合も同様です。デフォルトは single-float です。

●複素数

Common Lisp は複素数を扱うことができます。複素数は #C の後ろに実部と虚部のリストを付けて表します。簡単な例を示しましょう。

#C(5 -3)
#C(1.2 2.4)
#C(1/2 2/3)
#C(0.5 2/3) => #C(0.5 0.6666667)

実部と虚部の指定には、整数、浮動小数点数、分数を使うことができます。もし、実部と虚部が異なる種類(型)の数ならば、同じ種類の数になるよう変換されます。

●算術演算

ここでは簡単な算術演算を説明します。

+ は足し算を、* は掛け算を、- は引き算を行います。これらの関数は引数をいくつでも取ることができます。数以外のデータを引数に与えるとエラーになります。引数の型が異なる場合は強制的に型変換が行われます。簡単な例を示しましょう。

(+)           => 0
(+ 1)         => 1
(+ 1 2 3)     => 6
(+ 1 2 3 1/2) => 13/2
(+ 1 2 3 4.5) => 10.5

(*)           => 1
(* 1)         => 1
(* 1 2 3)     => 6
(* 1 2 3 1/4) => 3/2
(* 1 2 3 4.5) => 27.0

(- 1)         => -1
(- 10 5 4)    => 1
(- 10 5/2)    => 15/2
(- 10 4.5)    => 5.5
(-)           => エラー  ; 引数が足りない

/ は割り算を行います。整数同士の割り算で割り切れない場合は分数になります。引数が 0 の場合はエラーになります。

(/ 2)     => 1/2    ; 引数の逆数を求める
(/ 8 4 2) => 1      ; 約分されて整数になる
(/)       => エラー ; 引数が足りない

整数や分数を浮動小数点数に変換するには関数 float を使います。

(float 1/3) => 0.3333333
(float 3)   => 3.0

逆に、分数や浮動小数点数を整数に変換するには、次に示す関数を使います。

引数 num2 が与えられた場合は (/ num1 num2) を評価し、その結果を整数に変換します。

1+ は引数に 1 を加え、1- は引数から 1 を引きます。

(1+ 2)   => 3
(1+ 2.5) => 3.5
(1- 2)   => 1
(1- 2.5) => 1.5

incfdecf は次の S 式と同じ働きをします。

(incf a)   ≡ (setf a (1+ a))
(incf a n) ≡ (setf a (+ a n))
(decf a)   ≡ (setf a (1- a))
(decf a n) ≡ (setf a (- a n))

incf は第 2 引数が省略されると、第 1 引数のシンボルに格納されている数値に 1 を加えます。第 2 引数が与えられると、その値を加えます。incf は足し算した結果を返します。decf は incf とは逆に引き算を行います。

gcd はすべての引数の最大公約数を返します。lcm はその引数の最小公倍数を返します。どちらの関数も引数は整数でなければいけません。

(gcd 91 49)     => 7
(gcd 63 42 35)  => 7
(lcm 14 35)     => 70
(lcm 1 2 3 4 5) => 60

関数定義

●defun は関数を定義する

Common Lisp で関数を定義する方法を説明します。簡単な例として、数を 2 乗する関数を作ってみます。

List 1 : 数を 2 乗する関数

(defun square (x) (* x x))

関数を定義するときは defun (DEfine FUNction) を使います。defun の構文を下図に示します。

defun は数式と比較するとわかりやすいでしょう。

それでは、実際に実行してみます。

(defun square (x) (* x x))
=> square
(square 4)
=> 16

関数を定義するには名前が必要です。defun の次に関数名をシンボルで定義します。文字列や数値ではいけません。defun で定義された処理内容は、関数名で指定したシンボルに格納されます。

関数名の次にはリストが必要です。これをラムダリスト (lambda-list) といい、関数の引数名をシンボルで定義します。引数を取らない関数は空リスト () を設定します。それから、関数定義で使用する引数のことを仮引数、実際に与えられる引数を実引数といいます。square の定義で使用した x が仮引数で、(square 4) の 4 が実引数となります。

そして、最後に処理内容を定義します。square の処理内容は (* x x) の 1 つだけですが、defun では複数の S 式を定義することができます。この場合、S 式はリストに並べた順番で評価され、最後に評価された S 式の結果を、その関数の評価結果として返します。

defun は正常に関数を定義できたら、関数名のシンボルを返します。square を実行するには今まで説明したように、リストの先頭に square をセットしその後ろに引数を与えれば、square に定義された処理内容を実行できます。

●レキシカル変数とスペシャル変数

では、ここで変数 x に値が代入されている場合を考えてみましょう。

(setq x 100)
=> 100
(square 5) => ?

結果はどうなると思いますか。100 の 2 乗で 10000 になるのでしょうか。これはちゃんと 5 の 2 乗が計算されて、結果は 25 になります。そして、square を実行したあとでも x の値は変わりません。

(square 5)
=> 25
x
=> 100

関数 square の仮引数 x は、その関数が実行されている間だけ有効です。このような変数をレキシカル変数 (Lexical variable) もしくは局所変数といいます。これに対し、最初 x に値を代入した場合、その値は一時的なものではなく、その値はずっと残っています。このような変数をスペシャル変数 (Special variable) もしくはグローバル変数といいます。

Common Lisp はシンボルの値を求めるとき、それがレキシカル変数であればその値を使います。レキシカル変数でなければ、スペシャル変数の値を使います。次の例を見てください。

(setq x 10)
=> 10
(setq y 20)
=> 20
(defun foo (x) (print x) (print y))
=> foo

(foo 100)

100       <= print の出力
20 20     <= 最初は print の出力で、次が foo の返り値

print は S 式を出力する関数です。print は改行してから S 式を出力して、最後に空白文字を 1 文字つけます。print は出力した S 式をそのまま返します。

最初にスペシャル変数として x と y に値を代入します。関数 foo は仮引数として x を使います。foo を実行すると、x はレキシカル変数なので値は実引数の 100 になります。y はレキシカル変数ではないのでスペシャル変数の値 20 になります。この関係を図に示すと、次のようになります。

このように、スペシャル変数の値はレキシカル変数の値で隠されるわけです。Common Lisp では、変数の種類を見た目で区別できるように、スペシャル変数を '*' (アスタリスク) で囲む慣習があります。ここでは説明のために、スペシャル変数とレキシカル変数に同じシンボル x を使いましたが、スペシャル変数を定義する場合は慣習に従って *x* としたほうがよいでしょう。

●let はレキシカル変数を定義する

関数の仮引数はレキシカル変数として扱われますが、このほかに Common Lisp では、レキシカル変数を定義するための特殊形式 let が用意されています。let の構文を見てみましょう。

 (let ((変数1 初期値1)
       (変数2 初期値2)
        ・・・・・・
       (変数M 初期値M)) 
     S式1
     ・・・
     S式M)

let は関数の仮引数のように与えられた名前をレキシカル変数として扱い、あとに続く S 式を順番に評価します。変数は初期値を評価した値に初期化されます。初期値が省略されると変数は nil に初期化されます。定義されたレキシカル変数は、let の実行が終了するまで有効です。let は最後に評価した S 式の値を評価結果として返します。それでは、簡単な例を示しましょう。

(setq x 10)
=> 10
(let ((x 100)) (print x))
=> 100 100
x
=> 10

最初 x に 10 を代入します。この場合、変数 x はスペシャル変数として扱われます。次の let では、まず x をレキシカル変数として定義して 100 に初期化します。そのあと、(print x) を実行します。最初の 100 は、print が画面へ出力したもので、次の 100 が let の返り値です。let の終了後、変数 x の値は 10 のままです。let で定義した変数 x は、確かにレキシカル変数として扱われたことがわかります。


Copyright (C) 2000-2003 Makoto Hiroi
All rights reserved.

[ PrevPage | xyzzy Lisp | NextPage ]