Lisp を学ぼう (2001/12/12 版)

1. Lisp ( List Processer : リスト処理言語 )

Lisp は、関数型言語と呼ばれているプログラミング言語の一つです。

1.1 xlisp

xlisp は、Lisp の処理系の一つで、 Freeware ( 今風に言えば、Open Software ですね.. ) です。

1.1.1 xlisp の起動

xlisp の起動は、xlispwin をクリックするだけです。 そうすると、新しい window が開かれます。 そして、
XLISP-PLUS version 2.1g
Portions Copyright (c) 1988, by David Betz.
Modified by Thomas Almy and others.
というメッセージが出ます。
以下、xlisp とのやり取りは、この window 内で行われます。

1.1.2 xlisp の終了

xlisp を終了する場合は、window の中で、
(exit)
とします。つまり、'(' [左括弧], 'exit' という英単語, そして ')' [右括弧] の順に、キーボードから入力し、最後に、改行キー ( [Enter] キー ) を押すと、 window が消えます。

1.1.3 xlisp を少し利用してみよう

まず、xlisp で、 12 + 34 の計算をすることを考えます。このとき、 キーボードから
(+ 12 34)
と入力してみてください。もちろん、最後に、改行を押します。 すると、46 という結果が表示されると思います。これは、確かに、 12 + 34 の計算結果になると思います。
ここで、注意して欲しいのは、'+' と '12' と '34' の間には、空白が必要 で、逆に、'1' と '2', '3' と '4' の間には、空白を入れてはいけないという ことです。

[演習 1.1.1] xlisp を使って、123 + 567 の計算をしてみなさい。

[演習 1.1.2] xlisp を使って、123 * 567, 567 - 123, 567 / 123, 567 mod 123 をそれぞれ 計算しなさい。

[演習 1.1.3] xlisp を使って、1.23 * 5.67, 5.67 - 1.23, 5.67 / 1.23 をそれぞれ 計算しなさい。

1.1.4 複雑な計算

今度は、12 + 34 * 56 の計算を行います。これを行うには、次のように 入力します。
(+ 12 (* 34 56))
すると
1916
という答えが出てくると思います。

[演習 1.1.4] 123 + 567 * 89 の計算をしてみなさい。

[演習 1.1.5] 123 * 45 + 678 の計算をしなさい。

[演習 1.1.7] ( 123 + 567 ) * 89 の計算をしなさい。

[演習 1.1.7] ( 12 + 34 ) / ( 56 - 78 ) の計算をしなさい。

[演習 1.1.8] ( 3 - 5 * 6 ) / ( 1 + 2 ) の計算をしなさい。

1.2 lisp の言葉

lisp には、lisp 固有の言葉使いがあるので、これから lisp を学ぶ上で、 これらの言葉を予め覚えておくと良いでしょう。

[注意] 以下の説明では、所々、解り易さを優先するために、正確さに欠ける 表現が含まれている。詳しくは、各自、書籍や Web 等を当たって欲しい。
なお、この講義で、演習をするぶんには、以下の内容をとりあえず、信じて おいて構わない。

1.2.1 lisp での文字の種類

lisp では、文字の種類によって、固有の意味があります。実は色々とある のですが、まず、区別して欲しいのは、次の 3 種類です。
  1. 空白 : 空白 ( 漢字は許されない : xlisp では、日本語は利用できない と思っていて欲しい。) は、一つ以上、いくつ並んでも、一つの空白と 同じ意味になります。
  2. 括弧 : 左[開き]括弧 "(" と 右[閉じ]括弧 ")" は、常に対で利用 され、それぞれ単独で意味を持ちます。 "((" と書いてあった場合は、これは、飽くまでも、左括弧が二つある ものとみなされます。
  3. 英数字 : 英数字は、次に述べる atom を作る材料になります。一般に 英数字の並び ( 途中に空白を含めずに、英数字を並べたもの ) が atom になります。
    なお、演算記号 ( "+", "-", "/", "*", "%" ) は、原則として、 英字と同じようにして扱われます。
    特に、数字 ( と、必要に応じて、負の符号を表す "-", 小数点を表す "." [ピリオド], あるいは、分数を表す "/" を一つだけ含めても良い ) を空白を入れず に並べたものは、「数」として扱われます。
  4. クオート : シングルクオート記号 "'" は、単独で、特別な意味をもちます。 これに関しては、もう一度、「シングルクオート」のところで、説明します。 ので、ここでは、英数字、括弧、空白とは異なる種類の文字だということだけ を理解していてください。

1.2.2 atom (アトム)

atom は、上記で述べたように、基本は、「空白を含まない、英数字の並び (ただし、数値の場合は、小数点を表す"." や、分数を表す "/" が一つ含ま れても良い)」と考えます。実際には、これ以外のパターンもありますが、 しばらくは、atom とはこのようなものと思ってください。
atom は、更に、利用のされかたによって、次の 2 種類があります。
  1. 数値 atom : 数字ならびに、負の符号を表す "-", 小数点を表す "." (ピリオド), 分数を表す "/" だけからなる atom で、これは、数値を 表しています。
  2. symbol atom : 英字から始まり、英数字または、演算記号からなる文字列 で、空白や括弧、シングルークオートを含まない文字列のことを言います。
    これは、一般に、「名前」として利用されます。

1.2.3 list (リスト)

list は、lisp の中心となる表現です。list は、次のように「再帰的に定義」さ れます。

list の定義 :
0 個以上の atom あるいは list を、空白を挟んで、 並べ、左括弧と右括弧で挟んだものを list と呼ぶ。
左括弧と右括弧の間にある、atom あるいは list を、 この list 全体の「要素」と呼び、その要素数を「list の長さ」 と呼ぶ。

ここで、「再起的」と呼んだのは、この定義の中に、list が利用されている からです。これでは循環論法に見えるのですが、実際には、問題ありません。
以下に、list の例をいくつか、挙げておきます。
  1. "()" : 空リスト、左括弧と右括弧の間には何も入っていない、 特別なリスト(要素数が 0 )。
  2. "( ringo banana mikan )" : 三つの atom [ "ringo", "banana", "mikan" ] を要素として持つ list
  3. "( + 3 ( * 4 5 ) ) " : 二つの atom [ "+" と "3" ] と、 一つの list 要素 [ "( * 4 5 )" ] を含む list
  4. "( (a b) (c d (e f)) )" : 二つの list 要素 [ "(a b)" と "(c d (e f))" ] からなる list 二つめの list は更に、list 要素を持っていることに注意。

1.2.3 S-式

S-式 というのは、lisp で扱われるようなデータ全部のことです。
単純に言えば、atom と list の総称と考えてよいでしょう。

1.3 lisp の式と計算

lisp に置ける「式」とは、要するに S-式のことです。lisp は、S-式を与えると、 その式の値を計算して、その結果を返すようになっています。
S-式には、上述したように atom と list があり、それぞれ、意味が異なります。

1.3.1 atom の値

atom の値は次のようになっています。
  1. 数値 atom : これは、それ自身の表す数値が、値となります。 実際、単に数値 atom を単独に入力して、改行すると、それそのもの が値として表示されます。
    ただし、分数の場合は、約分された結果が値となります。
  2. Symbol atom : これには、更に次の可能性があります。
    1. 予約語 : 予め、値が決められている atom で、ここでは次の二つの 例だけを挙げておきます。
      1. T : 値は T で、これは、lisp の世界での 真偽値の「真[true]」を表します。
      2. NIL : 値は NIL で、これは、lisp の世界での 真偽値の「偽[false]」を表します。
    2. 予約語以外 : 予約語以外の symbol atom は、 「変数」と見なされます。
      「変数」の値は、設定されている場合と設定されていない場合が あり、設定されている場合は、その「設定されている値」が、 「symbol atom の値」になります。
      逆に、設定されていない場合は、 「誤り(error: unbound variable と表示される)」となります。 (任意の symbol atom に好きな値を割り当てる方法は、後述 )。

[演習 1.3.1] 次の数値 atom の値を求めなさい。
  1. 1
  2. -4/6
  3. 1.00

[演習 1.3.2] 次の symbol atom の値を求めなさい。
  1. T
  2. NIL
  3. PI
  4. NandakaWakaranai
  5. symbol-atom

1.3.2 list の値

list は、空 list か空 list 以外の list ( 要素を一つ以上含んでいる list ) かの、どちらかで、空 list の値は NIL です。
空 list 以外の list は「関数の計算」を表していると見なされます。
そして、list の最初の要素は、「関数名」を、残りの要素 ( 0 個も有り得る ) は、その関数の引数として扱われます。
「関数名」は、通常は、symbol atom で、しかも、その atom がどのような 関数を表しているかは、予め割り当てられている必要があります (任意の symbol atom に好きな関数を割り当てる方法は、後述 )。
もし、最初の要素が、symbol atom ではなかったり、あるいは、それに、 関数が割り当てられていなかった場合は、「誤り」になります。
List の二つ目以降は、関数の引数となりますが、引数は、予め評価され、値 が計算された後に、関数の引数として渡されます。
例えば、1 + 2 * 3 を表す list "(+ 1 (* 2 3)" を考えてみましょう。
これは、三つの要素 "+", "1", "(* 2 3)" からなります。ここで、最初の 要素 "+" は、確かに symbol atom であり、実は、最初から「足し算」を行う 関数が割り当てられています。その二つの引数が "1" と "(* 2 3)" なわけ です。
従って、この list の値を計算するためには、 「足し算をする関数が呼び出す」 わけですが、その前に、引数となる "1" と "(* 2 3)" の値が計算されます。 "1" の値は、これが数値 atom なので、数値の 1 が値ですが、二つ目の引数 は、再び list なので、その値は、関数の計算の結果となります。
ここでは、もちろん、list の最初の要素が "*" で、「掛け算」が割当たって おり、引数の "2", "3" の値もそれぞれ、数値の 2, 3 なので、結果は、6 と なり、それが、足し算の計算に渡されるわけです。
つまり,一般的には、

括弧の内側から計算される

という「原則」があります。
なお、関数には、予め、引数の個数や、その種類があります。その個数や種類が 間違っている場合も、「誤り」となります。

[演習 1.3.3] 次の list の値を求めなさい。
  1. ()
  2. (+ 1 2)
  3. (mod 5 2)
  4. (mod 5 2 9)
  5. (1)
  6. ((+ 1 2) 3 4)
  7. (NandakaWakaranai)

1.4 変数への値の割り当て

前述の様に、symbol atom の値は、「設定された値」です。そこで、ここでは、 変数への値の割り当て方法を説明します。
変数 x に対して、値 1 を割り当てるのは、次のようにします。
(setq x 1)
すると、結果として、設定された値である 1 が表示されますが、それ以降 symbol atom "x" には、"1" という値が設定されます。

[演習 1.4.1] 次の操作を順に行いなさい。
  1. zzzz の値を求めなさい
  2. (setq zzzz 123) としなさい
  3. 再度 zzzz の値を求めなさい
  4. (setq zzzz 567) としなさい
  5. 再度 zzzz の値を求めなさい
  6. (+ zzzz 2) の値を求めなさい
  7. (+ zzzz (* zzzz zzzz)) の値を求めなさい
  8. 一旦、xlisp を終了し、もう一度 xlisp を起動しなさい
  9. 再度 zzzz の値を求めなさい

1.5 関数の定義

list を評価する場合、list の最初の要素は、関数を表す symbol atom であり、 その symbol atom がどのような関数であるかは、予め割り当ててある必要が ありました。
ここでは、任意の symbol atom に好きな関数を割り当てる方法について説明 します。

1.5.1 常に 1 を返す関数 one

まずは、小手調に、引数のない関数 ( これを定数関数と呼びますが.. ) を 次のように定義します。
(defun one () 1)
この結果、表示されるのは、"one" という symbol atom ですが、これで、 one という関数が定義されます。
(one)
とすれば、1 という値が計算されます。 なお、
one
としても駄目であることに注意しましょう。また、
(setq one 2)
としても、"(one)" の値は、"1" のままであり、逆に、"one" の値は、"2" になることも確かめましょう。

[演習 1.5.1] 常に 10 を返す関数 ten を定義せよ

1.5.2 一つの引数を持ち、これに 1 を加えた値を返す inc

次に、一つの引数を持ち、これに 1 を加えた値を返す inc を返す 関数を定義します。つまり、"(inc 1)" は、"2", "(inc 10)" は、"11" になります。
(defun inc (n) (+ n 1))
ここで、前節の one との違いは、「引数があるので、その引数に "n" という 変数名を付けている」という点です。
その後ろの "(+ n 1)" に現れる "n" という atom symbol には、"(inc 10)" など と書いた場合の引数の値 "10" が、「自動的かつ、一時的に設定される」わけ です。

[演習 1.5.2] 実際に inc を定義し、次の値を計算しなさい。
  1. (inc 1)
  2. (inc 10)
  3. n

[演習 1.5.3] 次のような一引数関数を定義しなさい。
  1. 引数から 1 を引いた値を返す関数 dec
  2. 引数の 2 乗を計算する squre

1.5.3 複数の引数を持つ関数の定義

三つの数の和を計算する add3 は次のように定義します。
(defun add3 ( x y z ) ( + x ( + y z ) ) )

[演習 1.5.4] 実際に add3 を定義し、次の値を計算しなさい。
  1. (add3 1 2 3)
  2. (add3 1 2 3 4)

[演習 1.5.5] 次のような関数を定義しなさい。
  1. 二つの数の平均を計算する ave
  2. 三つの数の積を計算する mul3

1.6 ファイルからの読み込み

前回は、xlisp でいきなり、関数を定義し、それを実行しました。 この結果は、保存されていないので、もう一度、実行するには、 もう一度、定義から打ち直さねばならず、また、作成した、プログラム (lisp における関数定義のことです) を提出する場合も、もう一度 打ち直す必要があったわけです。
しかし、xlisp には、予め書き溜めておいた関数定義をまとめて、 読み込む機能があり、それが load 関数です。
load 関数は、(load "ファイル名") とすることによって、ファイル名 で指定されているファイルの中身をあたかも、キーボードから打ち込んだ のと同じように扱います。
したがって、そこに、関数定義を入れておけば、この load 関数 を利用することにって、毎回、同じ内容を打ち込まなくても済む わけです。
ただし、ファイルは、xlisp と同じフォルダに置く必要がある ことに注意しなさい。

[演習 1.6.1]
  1. 「演習 1.5.5」で作成したファイルを、実際に load 関数を利用して、読み込み、確かに、動くことを 確認しなさい。
  2. "fact.txt" を読み込んで、fact が利用できることを確認 しなさい。

1.7 論理演算と条件判定

1.7.1 真偽値を返す関数

lisp の関数には、条件判断を行い、その結果として、真偽値 ( 真 [ 条件が成立するという値 ... T になる ] と、 偽 [ 条件が成立していないという値 ... NIL になる ] ) を返す関数があります。
  1. 数値比較 : 数値の比較の結果を真偽値として返します。
    関数名 名称 引数の個数 関数の値
    = 等しい 2 2 つの引数の値が等しければ真[T]、そうでなければ偽[NIL](以下同様)
    /= 等しくない 2 2 つの引数の値が異なれば真[T]
    < 小さい 2 一つ目の引数の値が二つ目の引数の値より小さければ真
    <= 以下 2 一つ目の引数の値が二つ目の引数の値以下 ( 等しいか小さい ) ならば真
    > 大きい 2 一つ目の引数の値が二つ目の引数の値より大きければ真
    >= 以上 2 一つ目の引数の値が二つ目の引数の値以上 ( 等しいか大きい ) ならば真
  2. 属性判定 : 引数の属性の判定結果を真偽値として返します。
    関数名 名称 引数の個数 関数の値
    zerop ゼロ判定 1 引数の値が 0 ならば真
    numberp 数判定 1 引数の値の形式が数値形式ならば真
    plusp 正判定 1 引数の値が 正の数 ならば真
    minusp 負判定 1 引数の値が 負の数 ならば真
    null NIL 判定 1 引数の値が NIL ならば真
    atom atom 判定 1 引数の値が atom ならば真(*1)
    listp list 判定 1 引数の値が list ならば真(*1)
    [注 *1 : NIL は、atom であると同時に、List でもある唯一の存在です。]
  3. Symbol の比較 : 数値以外も比較可能です。
    関数名 名称 引数の個数 関数の値
    eq 同一性比較 ( identity ) 2 二つ引数の値が同じ物ならば真(*2)
    equal 同一形式 ( equality ) 2 二つ引数の値が同じ表現ならば真(*2)
    [注 *2 : この区別はし難いかもしれませんが、例えていえば、 「双子は、equal ( 同じように見える ) だが、eq ( 同一人物 ) ではない」 というところでしょうか..。
    一般に、eq なら、equal が常に成立するのですが、その逆は、成立するという 保証がありません。
    実際、atom は、equal なら eq が言えるのですが、 list の場合は、equal だからといって、eq が言えないことがほとんどです。 ( 偶然 言えることもありますが.. )。
    結論からいえば、「普通は、equal を使うのが無難」で、まあ、eq は使わないという ことでしょうか。 ]

[演習 1.7.1] 上記の関数に関して、成立する場合としない場合の例 を考え、実際に、その例を試してみなさい。

1.7.2 論理演算

与えられた真偽値から新しい真偽値を計算する演算を論理演算と 呼びます。
関数名 名称 引数の個数 関数の値
and かつ ( 論理積 ) 0... (*3) 引数のどれかの値が NIL なら偽 [NIL] 。そうでなければ、NIL 以外(*4)
or または ( 論理和 ) 0... (*3) すべての引数の値が NIL なら偽 [NIL] 。そうでなければ、NIL 以外(*5)
not でない ( 否定 ) 1 引数の値が NIL なら 真(*6)
[注 *3 : 引数は何個でも書けます。]
[注 *4 : 実際には、最後の引数の値になります。]
[注 *5 : 実際には、NIL でなかった最初の引数の値になります。]
[注 *6 : null と全く同じ働きをします。]

[演習 1.7.2] 上記の関数に関して、成立する場合としない場合の例 を考え、実際に、その例を試してみなさい。

[演習 1.7.3] and や or に、全く引数を与えない場合にどうなるか を考え、その上で、実際に、xlisp での計算を行って、その 考えが正しいかどうかを確かめなさい。

1.7.3 条件分岐

真偽値を利用して、挙動を変えたい時があります。その場合に利用するのが、 以下の関数です。
関数名 名称 引数の個数 関数の値
if もし 2 か 3 (*7) 一つ目の引数が NIL なら、全体の値は、三つ目の引数の値(*4)、 そうでなければ、二つ目の引数の値 (*8)
cond 多条件分岐 0 .. cond の引数は「二つの要素を持つ List」の形を想定しています。 cond の値は、次のように計算されます。
最初の List 引数の最初の要素の値が NIL でないならば、 その List の二番目の値、そうでなければ、次の要素の最初の要素の値 を調べる、以下、同様。
[注 *7 : 引数が 2 つの場合は、3 つ目の引数として NIL が与えられたと 同じように振舞います。(つまり一つ目の引数が NIL なら無条件に NIL になる)]
[注 *8 : より正確には、「引数を評価した値」となります。実は、次の cond ( や、 実は、and や or などや、defun など.. ) もそうですが、if は、普通の関数とは 異なり、呼び出される前に引数の値が評価されません ( 逆に、われわれが defun で定義する関数は、全て、呼び出される前に引数の評価が行われています )。 評価が行われるのは、関数が呼び出された後に、今回の例にあるように、 必要に応じて、積極的に評価が行われます。 従って、逆にいえば、引数の中には評価されないままのものも生じる可能性が あるということです。
これは、奇異に感じるかもしれませんが、このような仕組みの必要性は、 「演習 1.7.4」をやってみれば解ると思います。 ]

[演習 1.7.4] 次の点を確かめなさい。
  1. (+ 1 2 3) はどうなるか ?
  2. (+ 1 NIL 3) はどうなるか ?
  3. (+ 1 (+ 1 NIL) 3) はどうなるか ?
  4. (if NIL (+ 1 NIL) 3) はどうなるか ?
  5. (if T (+ 1 NIL) 3) はどうなるか ?
  6. (and T (+ 1 NIL) 3) はどうなるか ?
  7. (or T (+ 1 NIL) 3) はどうなるか ?

[演習 1.7.5] 引数が正の数かどうかを判定する関数 pos-num を作りたい。 すなわち、「引数が正の数ならば、T そうでなければ NIL を返す」とする。 この時、次の問いに答えなさい。
  1. なぜ、関数 plusp では駄目なのだろうか ( ヒント : (plusp T) の結果は ? )
  2. if, numberp, plusp を利用して、 pos-num を定義しなさい。

[演習 1.7.6] 絶対値の計算を行う関数 myabs を作りなさい。すなわち、 「引数が正の数ならそのまま、負の数ならその符号を変えたもの、そうでなければ 0 を返す」関数である。もちろん、数以外の引数が与えられた場合でも「エラーにならず、 0 を返す」ものとします。

[演習 1.7.7] myif という関数を次のように定義した
(defun myif (condition then else) ( if condition then else ) )
すなわち、「myif は、if の私家版」としたい。これは巧く行くだろうか ?

[演習 1.7.8] if と cond の関係を知るために、次の問いに答えなさい。
  1. (if a b c) と書かれている場所を if の代わりに、cond を利用して、書き変えるとしたら、どのようにすれば良いか ?
  2. [演習 1.7.5] の pos-num を、if の代わりに cond を利用して書き換えなさい。
  3. (少し難しい)
    同様に、if, cond の代わりに、and と or の両方を利用して、pos-num を定義してみなさい。

[演習 1.7.9] 二番目の引数の値によって、挙動を変える次のような関数 mcalc を定義しなさい。
  1. 引数は 3 つで、いずれも数である。
  2. 最初の引数はと三つめの引数は任意の数が入り、これが計算の対象となる。
  3. 二つ目の引数は、整数で次のような意味がある。
    1. 値が 1 の時、関数の値は、一つ目と三つ目の和になる。
    2. 値が 2 の時、関数の値は、一つ目と三つ目の差になる。
    3. 値が 3 の時、関数の値は、一つ目と三つ目の積になる。
    4. 値が 4 の時、関数の値は、一つ目と三つ目の商になる。
    5. その他の時、関数の値は、0 になる。

1.8 関数の定義(その 2)

1.8.1 定義した関数の呼び出し

defun を用いて定義した、関数は、さらに、他の関数を defun するときに、利用できます。
これによって、関数を少しずつ、複雑に、高機能にすることが できます。

[演習 1.8.1] 「演習 1.7.9」を次のような形で実現する。
  1. まず、次の 4 つの関数を作りなさい。
    1. myadd : 引数は二つで、足し算を行う。ただし、 + とことなり、引数に数以外が指定されたら 0 と とする。
    2. mysub : 引き算。後は、myadd に同じ
    3. mymul : 掛け算。後は、myadd に同じ
    4. mydiv : 割り算。後は、myadd に同じだが、 0 で割り算をしても、結果を 0 とする。
  2. 上記の 4 つを利用して、「演習 1.7.9」と同じ機能 の mcalc2 を作れ。

1.8.2 再帰呼び出し

実は、lisp では、現在定義中の関数を、その定義の中で 呼び出すことができる ( list の定義を思い出そう.. )。 ここでは例を示すので、これを参考にして、課題を解いて欲しい。

[演習 1.8.2] 次の定義を打ち込み、何がおきるかを 考えなさい。
  1. 階乗を計算する関数 fact の定義は次のようになる
    (defun fact (n) (if (plusp n) (* n (fact (- n 1))) 1 ))
  2. 次の計算をしてみなさい。
    (fact 6)

[演習 1.8.3] 一つ前の演習を参考に、階和を計算する 関数、sum を定義しなさい。

[演習 1.8.4] 二つの引数の最大公約数を計算する gcm を 定義しなさい。

2. list 処理

ここまでは、構造を持たないデータ (数値、真偽値 : いずれも atom) を対象とした計算について説明してきたが、lisp の本来の姿は、 list processer の名にもあるように list 処理にある。
この章では、lisp の本来の姿を学んで欲しい。

2.1 quote

lisp では、list を与えるとそれは、関数の呼び出しであると 解釈され、計算された。
特に、atom そのもの与えても、「atom の値」を求められた ことに注意して欲しい。
ここで、もし「atom そのもの」あるいは「list そのもの」を をあらわしたい場合はどうなるだろうか ? その場合に利用 されるのが quote 「'」である。
atom あるいは list の前に ' をつけることによって、 「そのもの」をあらわすことができる。

[演習 2.1.1] 次の入力を行い、どのような結果が表示 されるかを確認しなさい。
  1. (setq a 1)
  2. a
  3. 'a
  4. (add 1 2)
  5. '(add 1 2)
  6. (atom (add 1 2))
  7. (atom '(add 1 2))
  8. (listp (add 1 2))
  9. (listp '(add 1 2))

2.2 list 処理の基本関数

list 処理の基本関数は、歴史的な理由から奇妙な名前を持つ、次の三つの関数 である。
  1. car : list の先頭の要素を返す。 この結果を元の list の 「car 部」と呼ぶ。
  2. cdr : list の先頭の要素を取り除いた残りの部分を返す。 この結果を元の list の 「cdr 部」と呼ぶ。
  3. cons : 二つの引数 ( 二つ目は list ) を持ち、 最初の引数を car 部、二つ目の 引数を cdr 部に持つ list を作る。
一般に、次の等式が成立する。但し、L は任意の list, X は、任意の S-式である。
  1. (car (cons X L)) = X
  2. (cdr (cons X L)) = L
  3. (cons (car L) (cdr L)) = L

2.2.1 car

car は、NIL 以外の list を引数とし、その list の要素先頭の要素を 値として返す。
別の言い方をすれば、関数 car は、list の car 部を返す関数である。
car の引数は NIL でない list である必要がある。car に atom を与えた 場合は、一般にエラーとなる。
[注意: xlisp の場合、car に NIL を与えると NIL になるようである。 しかし、これは、一般の lisp とは異なる振る舞いである ]

[演習 2.2.1] 次の入力を行い、どのような結果が表示 されるかを確認しなさい。
  1. (car '(a b c))
  2. (car '(a))
  3. (car NIL)
  4. (car 'a)
  5. (car '(add 1 2))
  6. (car (add 1 2))
  7. (setq l '(x y z))
  8. (car l)

2.2.2 cdr

cdr は、NIL 以外の list を引数とし、その list の要素先頭の要素を 取り除いた、残りの部分を値として返す。
別の言い方をすれば、関数 cdr は、list の cdr 部を返す関数である。
cdr の引数は NIL でない list である必要がある。cdr に atom を与えた 場合は、一般にエラーとなる。
[注意: xlisp の場合、cdr に NIL を与えると NIL になるようである。 しかし、これは、一般の lisp とは異なる振る舞いである ]
cdr の結果は、一般に list になることに注意しよう。

[演習 2.2.2] 次の入力を行い、どのような結果が表示 されるかを確認しなさい。
  1. (cdr '(a b c))
  2. (cdr '(a))
  3. (cdr NIL)
  4. (cdr 'a)
  5. (cdr '(add 1 2))
  6. (cdr (add 1 2))
  7. (setq l '(x y z))
  8. (cdr l)

[演習 2.2.3] car と cdr を用いて、次のような関数を定義しなさい。
  1. list の最初の要素を取り出す関数 ( つまり car と同じ ) first

    (first '(a b c)) の結果は a

  2. list の二番目の要素を取り出す関数 second

    (second '(a b c)) の結果は b

  3. list の三番目の要素を取り出す関数 third

    (third '(a b c)) の結果は c

2.2.3 cons

cons は、二つの引数を持ち、二つ目の引数は、一般に list である。
[注意: 実は、cons の第二引数は、list でなくてもかまわない。 しかし、この演習では、cons の第二引数に list 以外のものを 指定する場合については、扱わない。詳しくは、専門書を参照の こと]
cons は、一つ目の引数が car 部、二つ目の引数が cdr 部となる ような list を返す。

[演習 2.2.4] 次の入力を行い、どのような結果が表示 されるかを確認しなさい。
  1. (cons 'a '(b c))
  2. (cons 'a NIL)
  3. (cons '(a b c) '(d e f))
  4. (setq l '(x y z))
  5. (car (cons 'a l))
  6. (cdr (cons 'a l))
  7. (cons (car l) (cdr l))
  8. (eq l (cons (car l) (cdr l)))
  9. (equal l (cons (car l) (cdr l)))

[演習 2.2.5] car, cdr, cons を用いて次のような関数を定義
  1. 二つの引数を持ち、その二つの要素からなる list を作る pair

    (pair 'a 'b) の結果は (a b)

    (pair '(a b c) '(x y z)) の結果は ((a b c) (x y z))

  2. 二つ以上の要素を持つ list の最初要素と二番目の要素を交換する 関数 exchange

    (exchange '(a b c)) の結果は (b a c)

2.3 list 処理と再帰呼び出しのパターン

list は、再帰的に定義されたデータである ( list の定義を思い出そう.. ) したがって、一般に list を処理する関数は、自然に再帰呼び出しを行う 関数になる。
list 処理の基本は、次のような事実による。
  1. list は、NIL でない限り、car 部とcdr 部の二つに分けられる
  2. cdr 部は再び list になる (ので、再帰呼び出しが使える)
  3. car 部の処理をどうするかで、結果が異なる。
再帰呼び出しのパターンは、それこそ、様々な形式が考えられるが、ここでは 基本となる、パターンをいくつか紹介する。

2.3.1 走査型

list の中に特定の要素があるかどうかを調べるような関数は、全て、この 走査型に含まれる。
この走査型の関数の構造は次のようになっている。
  1. もし、引数が NIL なら、走査は失敗したので NIL を返す
  2. そうでなければ、car 部を調べる
    1. car 部が求めるものなら、走査終了、結果を返す。
    2. そうでなければ、 cdr 部を探す ( 再帰呼び出し )

[演習 2.3.1] 次の入力を行い、どのような結果が表示 されるかを確認しなさい。inatom は、list の要素に atom があるか どうかを調べ、 atom があれば、その atom を、そうでなければ、NIL を返す関数である。
  1. (defun inatom (L) (if (NULL L) NIL (if (atom (car L)) (car L) (inatom (cdr L)))))
  2. (inatom '( (a b) c (d e) ) )
  3. (inatom '( (a b) x (d e) y ) )
  4. (inatom '( (a b) (d c f) (d e) ) )

[演習 2.3.2] 次の入力を行い、どのような結果が表示 されるかを確認しなさい。関数 memberp は、二つの引数もち、 一つ目の引数が、二つ目の引数である、list に要素として含まれている かどうかを判定し、含まれていれば T そうでなければ NIL を 返す関数である。
  1. (defun memberp (X L) (if (NULL L) NIL (if (equal X (car L)) T (memberp X (cdr L)))))
  2. (memberp 'c '( (a b) c (d e) ) )
  3. (memberp 'c '( (a b) x (d e) ) )
  4. (memberp 'c '( (a b) (c) (d e) ) )

[演習 2.3.3] 次のような走査型関数を定義しなさい
  1. inlist : 一つの list を引数とし、その list の要素に list が 含まれていれば、T そうでなければ NIL を返す関数
  2. inplus : 一つの list を引数とし、その list の要素に 正の数が 含まれていれば、T そうでなければ NIL を返す関数
  3. ingt : 二つの引数を持ち、一つ目は、数、二つ目は、数の list とする。 この関数は、二つ目の引数の list の中に、一つ目の 引数より大きな数があれば、その値を返す。そうでなければ。一つ目の 数を返す。
  4. memberpp : 二つの引数を持ち、二つ目は list の list とする。 memberpp は、二つ目の list の要素 ( これは、list である ) の内 一つ目の要素を含む list があれば、それを返す。そうでなければ NIL を返す。
    [ヒント:memberp を利用してよい]

2.3.2 累積型

走査型では、途中で求めるものが見つかれば、その後を処理しない。 累積型では、原則として、list の要素を全て処理する。
そして、その処理した結果を、まとめたものを結果とする。 この走査型の関数の構造は次のようになっている。
  1. もし、引数が NIL なら、定値を返す
  2. そうでなければ、
    1. car 部を処理する
    2. cdr 部を処理する ( 再帰呼び出し )
    3. car 部の処理結果と cdr 部の処理結果を合わせた処理をし、全体の結果とする

[演習 2.3.4] 次の入力を行い、どのような結果が表示 されるかを確認しなさい。関数 alladd は、数の list を引数と し、その数の値を全て加えた結果を返す。
  1. (defun alladd (L) (if (NULL L) 0 (+ (car L) (alladd (cdr L)))))
  2. (alladd NIL )
  3. (alladd '(1 2 9 10) )
  4. (alladd '(9) )

[演習 2.3.5(mapcar 型)] 次の入力を行い、どのような結果が表示 されるかを確認しなさい。関数 mapsquare は、数の list を引数と し、要素をすべて二乗した結果の list 返す。
  1. (defun mapsquare (L) (if (NULL L) NIL (cons (* (car L) (car L)) (mapsquare (cdr L)))))
  2. (mapsquare '(2 3 5 9) )
  3. (mapsquare NIL )

[演習 2.3.6] 次のような累積型関数を定義しなさい
  1. listlen : 一つの list を引数とし、その要素数を数える
  2. maxpnum : 正の数からな list を与え、その中で最大の要素を返す
  3. allmul : 数の list を与え、その要素を全て掛けた結果を返す関数
  4. allcar : list の list を与え、その各要素の一つ目要素からなる list を返す。
    (allcar '( (a b c) (e f g) (x y z))) = (a e x)
  5. mulv : 二つの引数を持ち、一つ目は、数、二つ目は数の list とする 結果は、二つ目引数の各の要素に、一つ目数をそれぞれ掛けた結果の list とする。
    (mulv 3 '( 1 2 3 4 ) ) = (3 6 9 12)
    これは、list をベクトルとしてみたときの定数倍に相当する。
  6. addv : 二つの引数を持ち、ともに同じ数の要素を持つ 数 list とする。 結果は、二つの引数のそれぞれ n 番目同士の要素の和を結果とする list になる。
    (addv '(3 4 5) '(10 20 30) ) = (13 24 35)
    これは、list をベクトルとしてみたときのベクトルの和に相当する。
  7. prodv : addv と同様で、ベクトルの内積を計算する。

2.3.3 順累積型

順累積型は、累積型の一種である。但し、通常の累積型(逆累積型と..) の場 合、list の要素を 後ろの方 「cdr 部」から処理し、その後「car 部」を処理するといった形が 多い。これは、後から、「car 部とcdr 部の結果をまとめる」操作が、対称 な処理であれば、問題ない ( 加算とか.. )。しかし、そうでない場合 ( 例えば 減算.. ) は、「car 部を先に処理するする必要がある」場合もある。
特に cons は、引数に対称性がない、典型的な操作なので、このような 順累積型が必要になることがある。 この順累積型の関数の構造は次のようになっている。
  1. 順累積型は、main 関数と sub 関数の二つからなる。
  2. main 関数は、sub 関数に、引数が NIL のときの値と その他の引数全てを sub 関数に与える。
    この main が渡す値を累積値と呼ぶ
  3. sub 関数は次のような再帰で定義する
    1. もし、引数が NIL なら、累積値を返す
    2. そうでなければ、
      1. car 部を処理する
      2. car 部の処理結果と累積値の処理結果を求め、 新しい累積値とする。
      3. 新しい累積値を利用して cdr 部を処理する ( 再帰呼び出し )

[演習 2.3.7] 次の入力を行い、どのような結果が表示 されるかを確認しなさい。関数 revlist は、list の要素を の順を逆順にした結果を返す。
  1. (defun revlist (L) (revlistsub NIL L)
  2. (defun revlistsub (V L) (if (NULL L) V (revlistsub (cons (car L) V) (cdr L))))
  3. (revlist NIL )
  4. (revlist '(a b c))
  5. (revlist '((a b)(c d)(e f)))

[演習 2.3.8] maxnum : 少なくても一つの要素を含む数の list を与え、 その中で最大の要素を返す関数を定義しなさい。

[演習 2.3.9] 行列の計算をする関数群を考え、定義しなさい。

3. 小さな application 例

この章では、小さな lisp application を作成することにします。 具体的には、lisp を利用して、行列の処理を行う program を 作成してみます。

3.1 データ表現

lisp で application を作成する場合、まず、行わなければ ならないことは、「データの表現をどうするか」ということ です。
具体的には、ここで、扱いたい、行列やベクトルを、lisp で扱える形式 ( S-式 : list と atom ) の、どのように 形に対応させるかという問題です。

3.1.1 ベクトルの表現

ベクトル、すなわち、数の並びは、それほど悩む必要は ありません。すなわち、数の list で表現すればよいから です。
しかし、厳密に考えると、縦ベクトルと横ベクトルでは、 本来、異なるわけですから、一方を、単なる list にす るのであれば、他方を、これとは (区別 可能な.. ) 異なる形にする必要があります。
ここでは、あまり厳密には考えずに、原則として、縦 ベクトルだけを考え、これを数の list とすることに します。

[注意] 縦ベクトルと横ベクトルを区別して 扱いたい場合は、何らかの表現が必要になります。 ここで、重要なことは、それを表現する規則を自分なり に決めて統一的に扱うことです。
例えば、三つの要素が 3, 4, 9 の三次元縦ベクトルを (v 3 4 9) で表し、同じ要素をもつ横ベクトルを、 (h 3 4 9) で表す ( つまり、最初の要素が v なら 縦、 h なら横と考えます ) とします。そうすれば、 縦ベクトルと横ベクトルは、異なる表現になるので、 区別して扱うことが可能になるわけです。

[演習 3.1.1] 縦ベクトルの和を計算する関数 vadd に 関して、次の作業を行いなさい。
  1. matrix.txt に、vadd が定義されているので、 load を利用して、これを読み込みなさい。
  2. 二つの 3 次元縦ベクトル (1 2 3) と (4 5 6) の 和を vadd を利用して計算しなさい。具体的には、 次の式を入力しなさい。
    (vadd '(1 2 3) '(4 5 6))

[演習 3.1.2] ベクトルのスカラー倍を計算する sprod を 定義しなさい。
(sprod 3 '(3 5 9) ) の結果は、(9 15 27)

[演習 3.1.3] もし、縦ベクトルと横ベクトルを、それぞれ ( v x1 x2 x3 ), ( h x1 x2 x3 ) のように、先頭の要素で 区別した形で、表現するとしたとき、それに対応したベクトル の和を計算する vvadd, hvadd をそれぞれ作成しなさい。
(vvadd '( v 1 2 3 ) '( v 4 5 6 ) ) の値は ( v 5 7 9 )
(hvadd '( h 1 2 3 ) '( h 4 5 6 ) ) の値は ( h 5 7 9 )

3.1.2 行列の表現

m 行 n 列の行列は、lisp で、どのように扱えば良いでしょうか。 ここでも、様々な形を考えることができます。 例えば、3 行 2 列の行列で、各要素が、x11, x12, x13, x21, x22, x23 である行列を、( 3 2 x11 x12 x13 x21 x22 x23 ) と表現しても かまいませんし、また、列ベクトル (縦ベクトル) を、前節の ルールに従って、(x11 x21), (x12 x22), (x13 x23) と表現し、その list ((x11 x21) (x12 x22) (x13 x23)) を、行列と考えること も可能でしょう。
しかし、ここでは、ちょっと、奇妙に感じるかもしれませんが、 行ベクトルの list すなわち、( (x11 x12 x13) (x21 x22 x23) ) で表現することにします。
これは、行列と、縦ベクトルの積が簡単に定義できるように するためです。

[演習 3.1.4] 行列の和を計算する madd を考え、定義しなさい(ヒント: vadd を利用)。
(madd '( (1 2 3) ( 4 5 6) (7 8 9) ) '( (1 -1 2) ( 2 1 -2) (-3 0 2))) の値は ((2 1 5) (6 6 4) (4 8 11))

[演習 3.1.5] 行列と縦ベクトルの積を計算する mvprod を考え、定義しなさい(ヒント: vprod を利用)。
(mvprod '((1 2 3)(4 5 6)(2 2 2) '(1 -1 2)) の値は (5 11 4)

[演習 3.1.6] 行ベクトルの list で表現されている行列 を、列ベクトルの list に変換する trans について、 次の問いに答えなさい。
  1. list の list が与えられたときに、各要素の car 部 だけを集めて作られる list を返す関数 carlist を 定義しなさい。
    (carlist '( (1 2 3) (4 5 6) (7 8 9))) の結果は、 (1 4 7)
    この関数は、行列の最初の縦ベクトルを求める ことに、注意しなさい。
  2. list の list が与えられたときに、各要素の cdr 部 だけを集めて作られる list を返す関数 cdrlist を 定義しなさい。
    (cdrlist '( (1 2 3) (4 5 6) (7 8 9))) の結果は、 ((2 3) (5 6) (8 9))
    この関数は、行列の最初の縦ベクトル取り除いた 行列を求めることに、注意しなさい。
  3. 上記の carlist, cdrlist を利用して、trans を実現 しなさい。
    (trans '( (1 2 3) (4 5 6) (7 8 9))) の結果は、 ((1 4 7) (2 5 8) (3 6 9))
  4. trans を二回適用するとどうなるかを考えなさい。
    (trans (trans '( (1 2 3) (4 5 6) (7 8 9))))
    の結果は、どうなるだろうか ?

[演習 3.1.7] 行列の掛け算を計算する mprod について、 次の問いに答えなさい。
  1. 最初の引数が、行ベクトルの list, 二つ目の 引数が、列ベクトルの list で表現されている 二つ行列の積を列ベクトルの list の形で求める 関数 mprodsub を考えなさい(ヒント: mvprod を利用)。
    (mprodsub '( (1 2) (3 1) ) '((5 6) (-1 1))) の結果は、 ((17 21) (1 -2))
  2. mprodsub と trans を組み合わせれば、mprod が 完成できる。実際に mprod を定義しなさい。
    (mprod '( (1 2) (3 1) ) '( (5 -1) ( 6 1 ) ) ) の値は、 ((17 1) (21 -2))

3.2 行列の application

行列の application として、逆行列の計算を行う mrev を考えます。 アルゴリズムとしては、ガウスの消去法を利用します。 また、問題を単純にするために引数で行列の次元を与える こと、また逆行列が存在しない場合は nil を返すものとします。

3.2.1 ガウスの消去法

ガウスの消去法は、逆行列を計算したい行列と、単位行列 を並べ、元の行列を単位行列に変化させるという作業 (掃出し) を行う際に、同じ操作を、並べてある単位行列に適用する ことによって実現します。
ここで、単位行列にするための操作は、原則として、次の三種類 があります。
  1. ある行に 0 でない数を掛ける
  2. m 行から n 行 ( m 以外 ) に定数を掛けた数を引く
  3. m 行と n 行を入れ替える
これらの操作は、実は、いずれも、可逆な作業であり、かつ、 同時に、実は、ある種の行列の掛け算 ( 可逆ということは、 すなわち、逆行列を持つ ) であることが、示されるわけですが、 これに関しては、線形代数学に譲るとして、ここでは、アルゴリズム として、これらの操作を考えます。 消去法では、まず、前進の掃出しと、後退の掃出しがあります。 前進が終了すれば、後退は、無条件に行えます。
前進消去は、行の順番に行います。ここで、処理する行の対角 要素が 0 の場合は、不都合があるので、他の行と交換する 必要があります ( 枢軸選び )。
もし、都合の良い行が見つからなかった場合は、その行列は、 正則でなかったことがわかり、そこでアルゴリズムは終了します。
全体の流れとしては、次のような手順になるでしょう。
  1. 与えられた行列と同じ次元の単位行列を用意する
  2. 前進消去を行う。その際に、同じ操作を、単位行列にも 適用する。
  3. 前進消去に成功したら、後退消去を行う。 その際にも、同じ操作を、単位行列にも適用する。

3.2.2 行列を操作するための基本関数

逆行列の計算を行う関数 mrev をいきなり作るのは大変なので 簡単な関数から次第にくみ上げて行く形にします。 このために、まずは、行列操作の基本関数から作ることに しましょう。

[演習 3.2.1] 行列を操作するための基本関数として、 次の三つの関数を定義しなさい。
  1. 行列 a の n 行目横ベクトルを返す関数 (nraw a n)
    (nraw '((1 2 3) (3 5 6) (7 8 9)) 2) の結果は (3 5 6)
    (nraw '((1 2 3) (3 5 6) (7 8 9)) 1) の結果は (1 2 3)
  2. 行列 a の n 行、m 列の要素を返す関数 (nmelm a n m)
    (nmelm '((1 2 3) (3 5 6) (7 8 9)) 2 1) の結果は 3
    (nmelm '((1 2 3) (3 5 6) (7 8 9)) 1 2) の結果は 2
  3. 行列 a の n 行目横ベクトルを v に変更した結果を 返す関数 (nreplace a n v)
    (nreplace '((1 2 3) (3 5 6) (7 8 9)) 2 '(0 0 0)) の結果は ((1 2 3) (0 0 0) (7 8 9))
    (nreplace '((1 2 3) (3 5 6) (7 8 9)) 3 '(0 0 0) ) の結果は ((1 2 3) (3 5 6) (0 0 0))

[演習 3.2.2] ガウスので利用する三つの基本操作を行う 関数を定義しなさい (ヒント:上記の nraw, nreplace を利用しよう..)。
  1. 行列 a の n 行目を c 倍する関数 (ncprod a n c)
    (ncprod '( (1 2 3) (4 5 6) (7 8 9) ) 2 3) の結果は ((1 2 3) (12 15 18) (7 8 9))
  2. 行列 a の n 行目から m 行目の c 倍を引く (nmcsub a n m c )
    (nmcsub '( (1 2 3) (4 5 6) (7 8 9) ) 3 1 7 ) の結果は ((1 2 3) (4 5 6) (0 -6 -12))
  3. 行列 a の n 行目と m 行目を交換する (nmchange a n m)
    (nmchange '((1 2 3)(4 5 6)(7 8 9)) 1 2) の結果は ((4 5 6) (1 2 3) (7 8 9))
    (nmchange '((1 2 3)(4 5 6)(7 8 9)) 3 2) の結果は ((1 2 3) (7 8 9) (4 5 6))

3.2.3 単位行列

以下の処理では、色々なところで、単位行列が必要になります。 そこで、n 次元の正方行列を求める関数があると便利です。
ここでは、その n 次元の正方行列を求める関数 (nunit n) を考えることにします。
さて、正方行列の計算ですが、これは、よく考えると、単位 ベクトルが並んでいるものと考えることができます。
したがって、最初に、 n 次元の、i 番目の単位ベクトルを返す を考えることにします。
(nunitv n i)
さて、n 次元の i 番目の単位ベクトルというのは、ようするに n 個の要素を持つ list で、i 番目が 1 で、その他の要素は、 0 となるようなベクトルです。
従って、n 個の要素を持つ list を、後ろから要素を決めながら、 作って行くことになります。
従って、その定義は次のようになります。
(defun nunitv (n i) (nunitvsub n i 1) )
(defun nunitvsub ( n i j ) (cond ( (< n j) nil ) ( t (cons (cond ( (= i j) 1 ) ( t 0 ) ) ( nunitvsub n i (+ j 1) ) )) ))
この nunitv があれば、nunit も同様に実現することができます。
(defun nunit ( n ) (nunitsub n 1) )
(defun nunitsub ( n i ) (cond ( (< n i) nil ) ( t (cons (nunitv n i) (nunitsub n (+ i 1))) ) ))

3.2.4 対処理

さて、ほぼ、逆行列の計算の本体部分に手を付ける準備が殆どでき あがってきたのですが、その前に、もう一つだけ、考えます。
この逆行列の計算では、与えられた n 次元の正方行列の要素を 消去しながら、その操作を、同時に、元々単位行列だった行列 に適用します。
もし、元の行列が単位行列になったら、今度は、その単位行列に 操作を加えたものが元の行列の逆行列になっているという点が、 このアルゴリズムの基本です。
従って、以下の関数では常に、元の行列と、元単位行列だった行列 の二つの行列を対にして扱います。
そこで、準備の最後として、二つのデータの対を扱う次の様な関数 を作成します。
  1. 二つの要素 a, b の対を保持するデータ構造を考える、具体的には、 これを、a, b をそれぞれ、第一要素、第二要素に持つ、長さ二の list で表現することにします。すると、次のような三つの関数を作ることが できます。
  2. (makepair a b) : a と b の対を作る。
    (makepair '(1 2) '(3 4)) の結果は、 ( (1 2) (3 4) )
  3. (pair1 p) : a と b の対 p の最初の要素 a を取り出す。
    (pair1 '((1 2) (3 4)) ) の結果は、 (1 2)
  4. (pair2 p) : a と b の対 p の二つ目の要素 b を取り出す。
    (pair2 '((1 2) (3 4)) ) の結果は、 (3 4)

[演習 3.2.3] 上記の makepair, pair1, pair2 を定義しなさい。

3.2.5 後退消去

計算の順番としては、最後になりますが、簡単な方から、 片づけることにして、後退消去から考えます。
後退消去は、すでに、上三角行列になっており、更に、 対角要素は、全て 1 となっている場合の処理です。
原則としては、下の列の要素から、順に 0 に消去して行きます。 その消去は、nmcsub で行うので、その処理は機械的です。 今、n 行の正方行列で、m 列目の消去を行う関数を、
(gbcnm n m p)
で表すとするとします。ただし、p は、元の正方行列と単位行列に にこれまでの操作を加えた結果を要素とするデータ対です、 これの定義は、次のようになります。
(defun gbcnm ( n m p ) ( gbcnmsub n m (+ m 1) p ) )
(defun gbcnmsub ( n m i p ) (cond ( (< n i) p ) ( t (gbcnmsub n m (+ i 1) (makepair (nmcsub (pair1 p) m i (nmelm (pair1 p) m i )) (nmcsub (pair2 p) m i (nmelm (pair1 p) m i )) )))))
この結果、例えば、
(gbcnm 3 2 (makepair '( (1 2 3) (0 1 2) (0 0 1) ) (nunit 3) ) )
の結果は、
(((1 2 3) (0 1 0) (0 0 1)) ((1 0 0) (0 1 -2) (0 0 1)))
になりますし、更に、
(gbcnm 3 1 (gbcnm 3 2 (makepair '( (1 2 3) (0 1 2) (0 0 1) ) (nunit 3) ) ) ))
の結果は、
(((1 0 0) (0 1 0) (0 0 1)) ((1 -2 1) (0 1 -2) (0 0 1)))
となり、確かに、一つ目の要素が、単位行列になっていることがわかります。 最後に、この計算結果の二つ目の要素と、最初の上三角行列の積
(mprod '( (1 2 3) (0 1 2) (0 0 1) ) '((1 -2 1) (0 1 -2) (0 0 1)))
を計算すると、確かに、単位行列になることがわかります。 後は、これを利用して、n 次元の行列のガウスの後退消去を行う
(gbc n p)
は、次のようになります( もちろん、p は、データ対です)。
(defun gbc ( n p ) ( gbcsub n (- n 1) p ) )
(defun gbcsub ( n i p ) (cond ( (< i 1) p ) ( t (gbcsub n (- i 1) (gbcnm n i p)) ) ))
これを使えば、
(gbc 3 (makepair '( (1 2 3) (0 1 2) (0 0 1) ) (nunit 3) ) ) の結果が、 (((1 0 0) (0 1 0) (0 0 1)) ((1 -2 1) (0 1 -2) (0 0 1)))
となり、後退消去がうまく行くことがわかります。

[演習 3.2.4] 次の、上三角行列の逆行列を求めなさい。また、それが本当に、 逆行列になっているかどうかを確認しなさい。
  1. ( (1 2) ( 0 1 ) )
  2. ( (1 2 3) (0 1 2) (0 0 1) )
  3. ( (1 2 3 4) (0 1 2 3) (0 0 1 2) (0 0 0 1) )

3.2.6 前進消去

前進消去は、基本的に、後退消去と同じ操作を、今度は、上から 下へと行い、上三角行列を作る作業です。 したがって、ここでも、活躍するのは、nmcsub です。
しかし、前進消去が、後退消去と大きく異なる点は、後退消去 が、無条件に行えるのに、前進消去は、場合によっては、 行えなかったり ( 正則でないと、途中でできなくなります )。 あるいは、操作を加える行を交換したりする ( 枢軸選び.. ) が必要になるという点です。
この点を注意しながら、順番に作って行くことにしましょう。
まず、考えるのは、後退消去と同様、m 行目の処理を行う関数
(gfcnm n m p)
です。引数の n, m, p は、gbc と同じです。また、ここでは p の一つ目に入っている消去の対象となる行列は、m - 1 行目 までは、すでに、上三角化されているものとして、処理します。
(defun gfcnm ( n m p ) ( gfcnmsub n m 1 p ) )
(defun gfcnmsub ( n m i p ) (cond ( (< n i) p ) ( t (gfcnmsub n m (+ i 1) (makepair (nmcsub (pair1 p) m i (nmelm (pair1 p) m i )) (nmcsub (pair2 p) m i (nmelm (pair1 p) m i )) )))))
これを利用すれば、例えば、次のような結果が得られます。
(gfcnm 3 3 (makepair '( (1 2 3) (0 1 6) (7 8 9) ) (nunit 3) ) ) の結果は、 (((1 2 3) (0 1 6) (0 0 -552)) ((1 0 0) (0 1 0) (161 -138 -23)))
次に考えるのは、正規化と、枢軸選びの問題です。
正規化は単に、対角要素を 1 にする操作で、これは、単に、対角要素 を含む行を、その対角要素で割ることで実現します。 但し、問題は、たまたま、その対角要素が 0 の場合です。この場合は、 当然のことながら、0 で割るということはできないので、困ってしまう わけです。もし、対角要素が 0 であれば、対角要素が 0 にならないように、 他の行と交換を行うことにします。これが枢軸選びです。
そこで、与えられた行列から、必要に応じて、枢軸を選び、正規化して、 返すような関数
( gfcnormal n m p )
を考えることにします。ここで、もし、枢軸が選べない場合は、nil を返すこと にします。 (defun gfcnormal ( n i p ) (cond ( (zerop (nmelm (pair1 p) i i)) (gfcnormalsub n i (gfcfind n i p)) ) ( t (gfcnormalsub n i p) ) ) )
(defun gfcnormalsub ( n i p ) (cond ( (null p) nil) ( t (makepair (ncprod (pair1 p) i (/ 1 (nmelm (pair1 p) i i))) (ncprod (pair2 p) i (/ 1 (nmelm (pair1 p) i i))) ) ) ) )
(defun gfcfind (n i p) (gfcfindsub n i (+ i 1) p) )
(defun gfcfindsub (n i j p) (cond ( (< n j) nil ) ( (zerop (nmelm (pair1 p) j i ) ) (gfcfindsub n i (+ j 1) p) ) ( t (makepair (nmchange (pair1 p) i j) (nmchange (pair2 p) i j) ) ) ))
ここで、gfcfind は、枢軸を探して、見つかれば、行を交換したもの を返します。もし、枢軸にふさわしい行がなければ、nil が帰ります。 gfcnormalsub は、枢軸が与えられていれば、正規化を行う関数です。
さて、いよいよ、前進消去が行える準備ができました。 前進消去と、後退消去の違いは、単に、途中で、正規化に失敗 した場合に、値として nil を返すというだけのことです。 定義は、以下のようになります。
(defun gfc ( n p ) ( gfcsub n 2 (gfcnormal n 1 p) ) )
(defun gfcsub ( n i p ) (cond ( (null p) nil ) ( (< n i) p ) ( t (gfcsub n (+ i 1) (gfcnormal n i (gfcnm n i p) ) ) ) ))
以上で準備はおしまいです。 逆行列の計算を行う関数 mrev は、次のような感じになります。
(defun mrev ( n m ) (mrevsub n (makepair m (nunit n))) )
(defun mrevsub ( n p ) (mrevsub2 n (gfc n p) ) )
(defun mrevsub2 ( n p ) (cond ( (null p) nil ) ( t (pair2 (gbc n p)) ) ) )

[演習 3.2.5] 様々な行列の逆行列を計算してみなさい。