Lisp は、関数型言語と呼ばれているプログラミング言語の一つです。
xlisp は、Lisp の処理系の一つで、 Freeware ( 今風に言えば、Open Software ですね.. ) です。
xlisp の起動は、xlwin32 をクリックするだけです。
そうすると、新しい window が開かれます。
そして、
XLISP-PLUS version 3.04
Portions Copyright (c) 1988, by David Betz.
Modified by Thomas Almy and others.
というメッセージが出ます。
以下、xlisp とのやり取りは、この window 内で行われます。
xlisp を終了する場合は、window の中で、
(exit)
とします。つまり、'(' [左括弧], 'exit' という英単語, そして ')' [右括弧] の順に、キーボードから入力し、最後に、改行キー ( [Enter] キー ) を押すと、 window が消えます。
まず、xlisp で、 12 + 34 の計算をすることを考えます。このとき、
キーボードから
(+ 12 34)
と入力してみてください。もちろん、最後に、改行を押します。
すると、46 という結果が表示されると思います。これは、確かに、
12 + 34 の計算結果になると思います。
ここで、注意して欲しいのは、'+' と '12' と '34' の間には、空白が必要
で、逆に、'1' と '2', '3' と '4' の間には、空白を入れてはいけないという
ことです。
今度は、12 + 34 * 56 の計算を行います。これを行うには、次のように
入力します。
(+ 12 (* 34 56))
すると
1916
という答えが出てくると思います。
lisp には、lisp 固有の言葉使いがあるので、これから lisp を学ぶ上で、 これらの言葉を予め覚えておくと良いでしょう。
lisp では、文字の種類によって、固有の意味があります。実は色々とある のですが、まず、区別して欲しいのは、次の 3 種類です。
atom は、上記で述べたように、基本は、「空白を含まない、英数字の並び
(ただし、数値の場合は、小数点を表す"." や、分数を表す "/" が一つ含ま
れても良い)」と考えます。実際には、これ以外のパターンもありますが、
しばらくは、atom とはこのようなものと思ってください。
atom は、更に、利用のされかたによって、次の 2 種類があります。
list は、lisp の中心となる表現です。list は、次のように「再帰的に定義」さ れます。
ここで、「再起的」と呼んだのは、この定義の中に、list が利用されている
からです。これでは循環論法に見えるのですが、実際には、問題ありません。
以下に、list の例をいくつか、挙げておきます。
S-式 というのは、lisp で扱われるようなデータ全部のことです。
単純に言えば、atom と list の総称と考えてよいでしょう。
lisp に置ける「式」とは、要するに S-式のことです。lisp は、S-式を与えると、
その式の値を計算して、その結果を返すようになっています。
S-式には、上述したように atom と list があり、それぞれ、意味が異なります。
atom の値は次のようになっています。
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 と
なり、それが、足し算の計算に渡されるわけです。
つまり,一般的には、
という「原則」があります。
なお、関数には、予め、引数の個数や、その種類があります。その個数や種類が
間違っている場合も、「誤り」となります。
前述の様に、symbol atom の値は、「設定された値」です。そこで、ここでは、
変数への値の割り当て方法を説明します。
変数 x に対して、値 1 を割り当てるのは、次のようにします。
(setq x 1)
すると、結果として、設定された値である 1 が表示されますが、それ以降
symbol atom "x" には、"1" という値が設定されます。
list を評価する場合、list の最初の要素は、関数を表す symbol atom であり、
その symbol atom がどのような関数であるかは、予め割り当ててある必要が
ありました。
ここでは、任意の symbol atom に好きな関数を割り当てる方法について説明
します。
まずは、小手調に、引数のない関数 ( これを定数関数と呼びますが.. ) を
次のように定義します。
(defun one () 1)
この結果、表示されるのは、"one" という symbol atom ですが、これで、
one という関数が定義されます。
(one)
とすれば、1 という値が計算されます。
なお、
one
としても駄目であることに注意しましょう。また、
(setq one 2)
としても、"(one)" の値は、"1" のままであり、逆に、"one" の値は、"2"
になることも確かめましょう。
次に、一つの引数を持ち、これに 1 を加えた値を返す inc を返す
関数を定義します。つまり、"(inc 1)" は、"2", "(inc 10)" は、"11"
になります。
(defun inc (n) (+ n 1))
ここで、前節の one との違いは、「引数があるので、その引数に "n" という
変数名を付けている」という点です。
その後ろの "(+ n 1)" に現れる "n" という atom symbol には、"(inc 10)" など
と書いた場合の引数の値 "10" が、「自動的かつ、一時的に設定される」わけ
です。
三つの数の和を計算する add3 は次のように定義します。
(defun add3 ( x y z ) ( + x ( + y z ) ) )
前回は、xlisp でいきなり、関数を定義し、それを実行しました。
この結果は、保存されていないので、もう一度、実行するには、
もう一度、定義から打ち直さねばならず、また、作成した、プログラム
(lisp における関数定義のことです) を提出する場合も、もう一度
打ち直す必要があったわけです。
しかし、xlisp には、予め書き溜めておいた関数定義をまとめて、
読み込む機能があり、それが load 関数です。
load 関数は、(load "ファイル名") とすることによって、ファイル名
で指定されているファイルの中身をあたかも、キーボードから打ち込んだ
のと同じように扱います。
したがって、そこに、関数定義を入れておけば、この load 関数
を利用することにって、毎回、同じ内容を打ち込まなくても済む
わけです。
ただし、ファイルは、xlisp と同じフォルダに置く必要がある
ことに注意しなさい。
lisp の関数には、条件判断を行い、その結果として、真偽値 ( 真 [ 条件が成立するという値 ... T になる ] と、 偽 [ 条件が成立していないという値 ... NIL になる ] ) を返す関数があります。
関数名 | 名称 | 引数の個数 | 関数の値 |
---|---|---|---|
= | 等しい | 2 | 2 つの引数の値が等しければ真[T]、そうでなければ偽[NIL](以下同様) |
/= | 等しくない | 2 | 2 つの引数の値が異なれば真[T] |
< | 小さい | 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) |
関数名 | 名称 | 引数の個数 | 関数の値 |
---|---|---|---|
eq | 同一性比較 ( identity ) | 2 | 二つ引数の値が同じ物ならば真(*2) |
equal | 同一形式 ( equality ) | 2 | 二つ引数の値が同じ表現ならば真(*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 と全く同じ働きをします。]
真偽値を利用して、挙動を変えたい時があります。その場合に利用するのが、 以下の関数です。
関数名 | 名称 | 引数の個数 | 関数の値 |
---|---|---|---|
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」をやってみれば解ると思います。
]
defun を用いて定義した、関数は、さらに、他の関数を defun
するときに、利用できます。
これによって、関数を少しずつ、複雑に、高機能にすることが
できます。
実は、lisp では、現在定義中の関数を、その定義の中で 呼び出すことができる ( list の定義を思い出そう.. )。 ここでは例を示すので、これを参考にして、課題を解いて欲しい。
ここまでは、構造を持たないデータ (数値、真偽値 : いずれも atom)
を対象とした計算について説明してきたが、lisp の本来の姿は、
list processer の名にもあるように list 処理にある。
この章では、lisp の本来の姿を学んで欲しい。
lisp では、list を与えるとそれは、関数の呼び出しであると
解釈され、計算された。
特に、atom そのもの与えても、「atom の値」を求められた
ことに注意して欲しい。
ここで、もし「atom そのもの」あるいは「list そのもの」を
をあらわしたい場合はどうなるだろうか ? その場合に利用
されるのが quote 「'」である。
atom あるいは list の前に ' をつけることによって、
「そのもの」をあらわすことができる。
list 処理の基本関数は、歴史的な理由から奇妙な名前を持つ、次の三つの関数 である。
一般に、次の等式が成立する。但し、L は任意の list, X は、任意の S-式である。
car は、NIL 以外の list を引数とし、その list の要素先頭の要素を
値として返す。
別の言い方をすれば、関数 car は、list の car 部を返す関数である。
car の引数は NIL でない list である必要がある。car に atom を与えた
場合は、一般にエラーとなる。
[注意: xlisp の場合、car に NIL を与えると NIL になるようである。
しかし、これは、一般の lisp とは異なる振る舞いである ]
cdr は、NIL 以外の list を引数とし、その list の要素先頭の要素を
取り除いた、残りの部分を値として返す。
別の言い方をすれば、関数 cdr は、list の cdr 部を返す関数である。
cdr の引数は NIL でない list である必要がある。cdr に atom を与えた
場合は、一般にエラーとなる。
[注意: xlisp の場合、cdr に NIL を与えると NIL になるようである。
しかし、これは、一般の lisp とは異なる振る舞いである ]
cdr の結果は、一般に list になることに注意しよう。
cons は、二つの引数を持ち、二つ目の引数は、一般に list である。
[注意: 実は、cons の第二引数は、list でなくてもかまわない。
しかし、この演習では、cons の第二引数に list 以外のものを
指定する場合については、扱わない。詳しくは、専門書を参照の
こと]
cons は、一つ目の引数が car 部、二つ目の引数が cdr 部となる
ような list を返す。
list は、再帰的に定義されたデータである ( list の定義を思い出そう.. )
したがって、一般に list を処理する関数は、自然に再帰呼び出しを行う
関数になる。
list 処理の基本は、次のような事実による。
再帰呼び出しのパターンは、それこそ、様々な形式が考えられるが、ここでは 基本となる、パターンをいくつか紹介する。
list の中に特定の要素があるかどうかを調べるような関数は、全て、この
走査型に含まれる。
この走査型の関数の構造は次のようになっている。
走査型では、途中で求めるものが見つかれば、その後を処理しない。
累積型では、原則として、list の要素を全て処理する。
そして、その処理した結果を、まとめたものを結果とする。
この走査型の関数の構造は次のようになっている。
順累積型は、累積型の一種である。但し、通常の累積型(逆累積型と..) の場
合、list の要素を
後ろの方 「cdr 部」から処理し、その後「car 部」を処理するといった形が
多い。これは、後から、「car 部とcdr 部の結果をまとめる」操作が、対称
な処理であれば、問題ない ( 加算とか.. )。しかし、そうでない場合 ( 例えば
減算.. ) は、「car 部を先に処理するする必要がある」場合もある。
特に cons は、引数に対称性がない、典型的な操作なので、このような
順累積型が必要になることがある。
この順累積型の関数の構造は次のようになっている。
この章では、小さな lisp application を作成することにします。 具体的には、lisp を利用して、行列の処理を行う program を 作成してみます。
lisp で application を作成する場合、まず、行わなければ
ならないことは、「データの表現をどうするか」ということ
です。
具体的には、ここで、扱いたい、行列やベクトルを、lisp
で扱える形式 ( S-式 : list と atom ) の、どのように
形に対応させるかという問題です。
ベクトル、すなわち、数の並びは、それほど悩む必要は
ありません。すなわち、数の list で表現すればよいから
です。
しかし、厳密に考えると、縦ベクトルと横ベクトルでは、
本来、異なるわけですから、一方を、単なる list にす
るのであれば、他方を、これとは (区別
可能な.. ) 異なる形にする必要があります。
ここでは、あまり厳密には考えずに、原則として、縦
ベクトルだけを考え、これを数の list とすることに
します。
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) )
で表現することにします。
これは、行列と、縦ベクトルの積が簡単に定義できるように
するためです。
行列の application として、逆行列の計算を行う mrev を考えます。 アルゴリズムとしては、ガウスの消去法を利用します。 また、問題を単純にするために引数で行列の次元を与える こと、また逆行列が存在しない場合は nil を返すものとします。
ガウスの消去法は、逆行列を計算したい行列と、単位行列
を並べ、元の行列を単位行列に変化させるという作業 (掃出し)
を行う際に、同じ操作を、並べてある単位行列に適用する
ことによって実現します。
ここで、単位行列にするための操作は、原則として、次の三種類
があります。
これらの操作は、実は、いずれも、可逆な作業であり、かつ、
同時に、実は、ある種の行列の掛け算 ( 可逆ということは、
すなわち、逆行列を持つ ) であることが、示されるわけですが、
これに関しては、線形代数学に譲るとして、ここでは、アルゴリズム
として、これらの操作を考えます。
消去法では、まず、前進の掃出しと、後退の掃出しがあります。
前進が終了すれば、後退は、無条件に行えます。
前進消去は、行の順番に行います。ここで、処理する行の対角
要素が 0 の場合は、不都合があるので、他の行と交換する
必要があります ( 枢軸選び )。
もし、都合の良い行が見つからなかった場合は、その行列は、
正則でなかったことがわかり、そこでアルゴリズムは終了します。
全体の流れとしては、次のような手順になるでしょう。
逆行列の計算を行う関数 mrev をいきなり作るのは大変なので 簡単な関数から次第にくみ上げて行く形にします。 このために、まずは、行列操作の基本関数から作ることに しましょう。
以下の処理では、色々なところで、単位行列が必要になります。
そこで、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))) ) ))
さて、ほぼ、逆行列の計算の本体部分に手を付ける準備が殆どでき
あがってきたのですが、その前に、もう一つだけ、考えます。
この逆行列の計算では、与えられた n 次元の正方行列の要素を
消去しながら、その操作を、同時に、元々単位行列だった行列
に適用します。
もし、元の行列が単位行列になったら、今度は、その単位行列に
操作を加えたものが元の行列の逆行列になっているという点が、
このアルゴリズムの基本です。
従って、以下の関数では常に、元の行列と、元単位行列だった行列
の二つの行列を対にして扱います。
そこで、準備の最後として、二つのデータの対を扱う次の様な関数
を作成します。
計算の順番としては、最後になりますが、簡単な方から、
片づけることにして、後退消去から考えます。
後退消去は、すでに、上三角行列になっており、更に、
対角要素は、全て 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)))
となり、後退消去がうまく行くことがわかります。
前進消去は、基本的に、後退消去と同じ操作を、今度は、上から
下へと行い、上三角行列を作る作業です。
したがって、ここでも、活躍するのは、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)) ) ) )