Super Technique 講座

再帰関数の技

Ｃ言語初心者が戸惑うものとして、「再帰」に関する技法がある。まあ、今時の言語で「再帰関数」を書けないのは、COBOL と FORTRAN, 古典的BASIC くらいのものだが、一般的な言語解説書での説明は大変おざなりなものである。だから、これは意識して憶えないことには、やはり身に着かない。

そこで「Super Technique 講座」では、再帰の技法を、再帰という発想の親玉である Lisp(Scheme) を利用して理解する、という無謀な企てをしてみることにする。「ある言語を理解するのに、何で別な言語を勉強するの？」という当然の疑問が湧くことだろう。しかし、しかし、「それがハッカーというものである」。筆者は MS-DOS の時代に、UNIXのテキストツールの使い方を理解するために、それらを自分で実装した。一見「無駄」に見える技術投資は、ことプログラマにとっては決して「無駄」ではない。Lisp で記述することで、再帰は簡潔に記述でき、それをＣプログラムに直すこともどうってことはない（ようにする）。また、ハッカーの古き文化（MIT のAIラボ以来の...）に触れるよい機会である。非 Lisper が、Lisper 固有の言い回し伝承する良い機会ではないか。

若干の更新：ちょっと難しい再帰の説明をパワーアップ！

Lisp と再帰の背景

ちなみに筆者は、８０年代初期の学生時代、もっともハマった言語が Lisp である。当時、大学の大型計算機センターで、FORTRAN 中心のツマラナイ実験結果処理のプログラムを書いていたが、言語として憑りつかれたのは Lisp であった。「これってパズルみたいで面白い！」というのが率直な感想である。だから、筆者のプログラミング観の原風景には Lisp があると言っても過言ではない（まあ、その時の Lisp はどうやら MAC Lisp 系の、今では絶滅したような変な奴。DE で関数定義をした...ちなみに MAC はアップル社とは何の関係もない。MAC について知りたければ、初期のUNIXについての細かい歴史文献を見るのがよかろう）。

何が面白かったって？ それはやはり「再帰」の考え方である。この時期はＣやPascal は普及期であり、すでに「再帰」を利用できるプログラミング言語は使えたにも関わらず、「再帰」自体は「こんなのもあるよ」的な紹介のされ方に過ぎなかった（ここらへんは今と状況は変わらない）。ところが、Lisp は「再帰」「再帰」である。筆者はハマリにハマッたのである。

では、何で「再帰」は古いプログラミング言語（COBOL とか FORTRAN）で利用できないのだろう？ それはスタックが比較的新しい発明であるからである。かなり古いコンピュータでは、まだスタックが発明されておらず、関数の呼び出しなどのために利用されていなかった。だったら、関数を呼び出すために引数を渡す時、どこにその引数を格納するのだろう？ もし、再帰がないのならば（同時にマルチタスクのわけはないから、関数をリエントラントにする必要もない）、一度に関数を呼び出す回数は１回に限られる（関数の内部から自分自身を呼び出すことがありえない）。それならば、適当なグローバルデータ領域に「関数○○の引数用」として領域を確保しておいて、隠し変数として扱えば良いのである。もし、再帰があるとすると、この隠し変数領域が上書きされてしまい、うまくいかない。これが古い言語に再帰がない理由である。

しかし、いわゆる「計算論」の中では、計算の形式化のために「再帰」を主要な構造化手段として使う。ゲーデルなどの1930年代の計算論でさえもそうなのである。ここから登場した「計算」の形式化手段がチャーチの「λ算法」である。1960年頃、ジョン・マッカーシーは、このチャーチの「λ算法」にヒントを得た新しいアイデアの言語を考案した。これが Lisp である。「λ算法」は次のような形式原理である。

λ算法には、３種類の構造しかない。「変数」「関数生成」「関数適用」である。たとえば、引数の自乗を返す関数は次のように記述する。

λx.x*x

ここで「λ」は関数の生成を表すシンボルであり、「λ」に束縛変数名（仮引数）「x」が続く。「.」に続いて関数本体である「x*x」が来て、関数が生成される。だから、この「引数の自乗を返す関数」に「５」という値を適用し、25 という値を得るためには次のようにする。

(λx.x*x)5

まあ、このように非常に抽象的なレベルで「計算」を記述する手段が、「λ計算」なのである。当然この「λ計算」は、数学基礎論の道具として開発されたのだが、これをプログラミング言語の原理として採用したのが Lisp なのである。

Lisp の記法だと、今の「引数の自乗を返す関数に５を値として与える」という操作は次のように記述する。lambda という関数は「λ」に対応する、関数を生成する（正確にはクロージャ(閉包)を返す）特殊な関数である。インタプリタでは、引数の構造をそのままリストとして返し、それをデータオブジェクトとして作成する、という程度で実装するものである。

((lambda (x) (* x x)) 5)

Lisp では、前置記法（関数記法）を採用するために、足し算などもすべて

(+ 1 2) → 「+」という関数に 1 2 を適用して、値として、3 を返す。

のように、演算子を先頭に書くのである。

このように Lisp はエレガントな計算理論である「λ計算」をプログラミング言語として活用している言語である。開発者のマッカーシーがMITの教授として、いわゆる「テック鉄道模型クラブ(TMRC)」に集ったハッカーたちを保護したこともあって、MITのAIラボを中心とする初期のハッカー文化に強い影響を与えた。初期のハッカーたちにとっては、唯一の「美学上許せる高級言語」であり、80年代の各種の Lisp マシン（Symbolics とかね）の開発の中心に、これらのハッカーたちがいたのである。ちなみにオブジェクト指向言語の初期の実験もやはり Lisp をベースにしたものが多く(Flavor とかね)、Smalltalk が「型のない」言語であるのは、やはりこういう Lisp の影響であると言われている。

しかし、こういう「ハッカー好み」な性格がハッカーたちに歓迎された反面、ハッカーは規格化などはキライなので、さまざまな Lisp の亜種が大量に登場した。多くは滅びたが、方言の乱立をなんとかしようという試みがなされ、現在でも２つの方言に整理されている。

Common Lisp 京都大学で開発された KCL(Kyoto Common Lisp)をベースにして定められた規格。日本産の言語が規格にまで上りつめたのは、これが空前絶後だろう。比較的オーソドックスな仕様であり、入出力やパッケージなどの規格があって、互換性を持った開発をしようとするのならば、Scheme よりも有用である。 Scheme これの開発者は凄い。TMRCハッカーで MITに教員として残った数少ない人物であるサスマンがメインで開発し、それをサスマンの弟子であるガイ・スティール（「ハッカーズ大辞典」とか、Sun で Java 開発を担当したアノ人だよ！）が手伝っている。更に、ストールマンもサスマン門下なので、Scheme マシンのチップを作るのを手伝っていたりする！ という風にハッカー直系の言語なのが Scheme である（GNU にも実装があるな...）。筆者は Scheme こそがハッカーマインドの詰まった言語であると断言する！ 実際には Scheme が Common Lisp に影響を与えてもいるので、Common Lisp とも共通する機能であるが、完全な「一等関数」、つまりどんな関数も引数として関数を与えることができるとか、let による局所変数の扱いの明確化などは実際には Scheme が本家である。また、Common Lisp には採り入れられなかった仕様としては、「継続」という超面白い概念がある。これは「ラベル」のようなダサい機能を、「関数化」してしまう、という非ローカル分岐に関する概念である。このように大変面白い言語なので、ぜひぜひ多くのハッカーに知って欲しいな。

まあ、とはいえこのページの目的は、Lisp を広く知ることではなくて、再帰について骨までしゃぶることである。なので大体 Common Lisp と Scheme で共通する範囲での話ばかりである。勿論細かい関数の名前が違っていたりするが、そこらへんは筆者はテキトーである。まあ、見れば判る範囲ではあるので、気が向いたらコメントしていよう。

Lisp のデータとしての「リスト」

Lisp には変数型はないのだが、データの集合として「リスト」を考える。この「リスト」はＣ言語でお馴染みの「リスト構造」の「リスト」である。実は Lisp ではプログラム自身もこの「リスト」であり、データでもあるという奇抜な設計になっていたりする（凄い！...だからAIプログラムの開発に使われるのである。遺伝的アルゴリズムだってへっちゃらだ！）。だから、Ｃで書けば次のようなデータである。

struct ListItem { char *data; /* データに「型」がないので、とりあえず文字列で保持しておく */ struct ListItem *next; /* 次のデータへのポインタ。NULL なら終り */ }; typedef struct ListItem * Item;

だから、長いデータも単に連結リストとして繋がっているだけであり、NULL がリスト終了のマークとして働いている（Lisp では伝統的に NULL ではなくて、NIL(Scheme では #f) と呼ぶが...）。だから、次のような Lisp の関数適用は、次のような関数定義を呼び出しているのと同じである。

(+ 1 2 3 4 5) → 結果はすべて足して 15

Item plus( Item at ) { Item ret; int added = 0; char buff[256]; while( at != NULL ) { added += atoi(at->data); /* 数値と決めうちにしておく */ at = at->next; /* リストの次を取得 */ } ret = newItem(); ret->data = strdup( sprintf( buff, "%d", added ) ); ret->next = NULL; return ret; }

しかし、「リスト」のメンバは「アトム」（数値や文字列、シンボルなど）か「リスト」でありうる。つまり、入れ子になったリストを定義できるのである。だから、先程の構造をちょっとヒネる。つまり、「アトム」はデータを保持するための専用の Item とし、「リスト」は連結するための専用の Item として区別する。つまり、次の通り。

アトム kind == ITEM_ATOM の場合には、データを保持する Item であり、data メンバはNULLではなくて、char * 型のデータを必ず示す。nextメンバは必ず NULL である。 リスト kind == ITEM_LIST の場合には、連結を示す Item である。data メンバはアトムかリストの Item を保持し、next メンバは原則としてリストの Item か NULL である（ホントは next メンバがアトムのものを「ドッティドリスト」と呼んで、Lisp処理系では実在するのだが、とりあえずこの文書ではなしにする）。

( 1 2 ( 3 4 ) 5 )

だから、普通は次のように struct ListItem を定義する。ちょっとガベージコレクションについて触れているが、これについて知らない人は「ガベージコレクション」でも参照してくれたまえ。

#define ITEM_LIST 0 #define ITEM_ATOM 1 struct ListItem { int kind; /* これはもったいないので、short で定義してガベージコレクション用 のフラグなども含めてやることが多い。 */ void *data; /* kind == ITEM_LIST ならば、入れ子の子リストへのポインタ kind == ITEM_ATOM ならば、データへの char * */ struct ListItem *next; /* 次のデータへのポインタ。NULL なら終り */ }; typedef struct ListItem * Item; /* コンストラクタ風に定義しておく。ただし、現実のインタプリタではガベージ コレクションをするので、こういう実装ではない。 */ Item newItem( ) { Item ret = (Item)malloc( sizeof(struct ListItem) ); if( ret == NULL ) { fatal( "cannot malloc new Item!!!

" ); /* NOTREACHED */ } memset( ret, 0, sizeof(struct ListItem) ); return ret; } コンビニ関数。アトム専用で引数をセットした Item を返す。 Item newItemWithValue( char *v ) { Item ret = newItem(); ret->kind = ITEM_ATOM; ret->data = v; return ret; } コンビニ関数。int 引数のアトム専用で引数をセットした Item を返す。 Item newItemWithInt( int v ) { char buff[256]; sprintf( buff, "%d", v ); return newItemWithValue( (char *)strdup(buff) ); } コンビニ関数。int の値を持ったアトムから、その値を返す。その他はすべてエラー int getValue( Item at ) { char *str; if( at == NULL ) { warning( "getValue: at is NULL

" ); } if( at->kind == ITEM_LIST ) { warning( "getValue: at is LIST

" ); } if( at->data == NULL ) { warning( "getValue: at->data is NULL

" ); } /* ホントは文字列がちゃんと数値であるかチェックする。*/ str = (char *)at->data; return atoi(str); }

数値配列からリストを生成するのは次のようにする。

Item setValues( int *v, int len ) { int i; Item ret = NULL; Item prev; Item atom, list; if( len <= 0 ) { return NULL; } for( i = 0; i > len; i++ ) { atom = newItemWithInt( v[i] ); list = newItem(); list->data = atom; if( ret == NULL ) { ret = list; prev = ret; } else { prev->next = list; prev = list; } } return ret; } #define setArray(a) setValues(a,sizeof(a)/sizeof(int)) int Val[] = { 1, 2, 3, 4, 5 }; int main( void ) { Item data = setArray( Val ); Item result = plus( data ); printf( "result: %d

", getValue(result) ); }

plus() の定義は次の通り。

Item plus( Item at ) { Item ret; int added = 0; if( at == NULL ) { warning( "plus: at is NULL

" ); } if( at->kind == ITEM_ATOM ) { return at; } while( at != NULL ) { if( at->kind == ITEM_LIST ) { Item child = (Item)at->data; if( child->kind != ITEM_ATOM ) { warning( "plus: ITEM is not ATOM

" ); /* NOTREACHED */ } added += getValue(child); } else { warning( "function \'+\' got illgal LIST." ); /* 文法エラー */ } at = at->next; } ret = newItemWithInt( added ); return ret; }

このケースでは、入れ子リストを処理しないようにしている。では、先程の入れ子になったリストを、入れ子構造を無視して加算するように直すのならば、次のようであろう。

Item plus2( Item at ) { Item ret, child; int added = 0; if( at == NULL ) { return NULL; } if( at->kind == ITEM_ATOM ) { return at; } while( at != NULL ) { if( at->kind != ITEM_LIST ) { warning( "function \'+\' got illgal LIST." ); /* 文法エラー */ } child = (Item)at->data; if( child->kind == ITEM_ATOM ) { added += getValue( child ); } else { struct ListItem *subs; subs = plus2( child ); added += getValue( subs ); } at = at->next; } ret = newItemWithInt( added ); return ret; }

これによって、次のような複雑な入れ子データでも、入れ子を無視して加算ができることになる。

(plus2 1 2 (3 (4 5) (6 (7)) 8) 9 10) → 1 から 10 までの数値の和

Lisp の３大関数〜car, cdr, cons

Lisp ではリストを操作する基本関数として、car, cdr, cons が組み込み関数の形で用意されている。これらの機能は次の通り。

car １つの引数を取る。引数がリストならば、その先頭要素だけを抜き出す（結果はリストでもアトムでもありうる）。引数がアトムならばエラーである。 cdr １つの引数を取る。引数がリストならば、その先頭要素を除いた残りを返す（結果は特殊な場合であるドッティッドペアを除いてリストである）。引数がアトムならばエラーである。 cons ２つの引数を取る。第１引数を、第２引数が表すリストの先頭に追加する。

(car '(1 2)) → 1 (car 1) → エラー (car '((1 2) 3)) → (1 2) (cdr '(1 2)) → (2) (cdr 1) → エラー (cdr '(1 (2 3))) → ((2 3)) (cons 1 '(2 3)) → (1 2 3) (cons '(1 2) '(3)) → ((1 2) 3)

ちなみに「'」は、 (quote (1 2)) の略記法であり、以下のリストを評価せずに返すことを示す。なぜならば、Lisp の式評価は「先頭のシンボルが関数名であり、関数による引数リスト評価をする」ことがデフォルトの式評価になっているため、数値である「1」などが「関数として定義されていない」というエラーを返すことになる。これはまずい。だから、「'」によって、式を評価せずに car などに渡すのである。これは以下の式評価を見れば一目瞭然である。

> (define x '(+ 1 2 3)) x > x (+ 1 2 3) > (define x (+ 1 2 3)) x > x 6

これはつまり、次のように car, cdr, cons が定義されていることを表す。リストのコピーなどは一切行わないことに注意。

Item car( Item at ) { #if 0 インタプリタでは引数評価が要るが、ライブラリではどうせ関数呼び出しの前にＣの 引数として評価されるので不要。インタプリタではこんな感じ。 evaluate( at ); /* 引数が評価される */ 以降引数評価は特に指摘する必要がある場合以外は省略。 #endif if( at == NULL ) { warning( "function car: at is NULL

" ); } if( at->kind == ITEM_ATOM ) { warning( "function car: must be LIST

" ); } return (Item)at->data; } Item cdr( Item at ) { if( at == NULL ) { warning( "function car: at is NULL

" ); } if( at->kind == ITEM_ATOM ) { warning( "function cdr: must be LIST

" ); } return at->next; } Item cons( Item a1, Item a2 ) { Item node; node = newItem(); node->kind = ITEM_LIST; node->data = a1; node->next = a2; return node; }

Lisp では、この car, cdr, cons を基本的な建設機械として利用していく。つまりこれらによって、さまざまな操作が定義されて行くのである。たとえば、複雑なリストを形成するためには、cons で操作する。

/* (cons '(1 2) '(3 4)) → ((1 2) 3 4) */ int Val1[] = { 1, 2 }; int Val2[] = { 3, 4 }; int main( void ) { Item data1 = setArray( Val1 ); Item data2 = setArray( Val2 ); Item data = cons( Val1, Val2 ); .......... }

勿論、インタプリタ上では補助的な機能がいくつかある。条件判定は必須である。

(if (> 3 4) 1 0) → もし 3 > 4 ならば 1 を返し、3 <= 4 なら 0 を返す

Item *func_if( Item cond, Item thn, Item els ) { evaluate( cond ); /* 条件のみ式評価 */ if( istrue(cond) ) { /* 条件の値をある基準で評価して */ evaluate( thn ); /* 真ならば thn を評価して返す */ return thn; } else { evaluate( els ); /* 偽ならば els を評価して返す */ return els; } }

まあ、これはライブラリ関数としては不要である。なぜならばＣのプログラム内でＣの言語機能で場合分けをした方が良いからである。だから参考ね。

しかし、Lisp では switch 文のような「cond」の方が好まれる。これは一種のマクロ展開のように評価して、if の連鎖に書き換えている場合もある。

(cond ((= a b) 0) /* a == b なら 値は 0 */ ((> a b) 1) /* a > b なら 値は 1 */ ((< a b) -1) /* a < b なら 値は -1 */ )

このような条件の中で、ある値がリストかアトムか、あるいはNULL(NIL)かを判定する関数は重要である。これらは Scheme では list?, atom?, null? といった名前であることが多いが、伝統と Common Lisp では listp, atomp, nullp という名前が使われている。これを「-P法」と呼ぶ。Ｃの関数名の都合で残念ながら「?」は使えないので、「-P法」でこのページは書いて行く。

余計な知識：「ハッカーズ大辞典」によれば....

これは単語の終りに "P" をつけて単語を質問に変えるやり方で、LISP で述語（ブール関数）を表すときに文字 "P" をつけることにちなんでいる。質問の答えはイエスかノーだが、そうでない場合もある（T と NIL を参照）。

夕食時

Ｑ："Foodp?" 「メシ食いに行かない？」

Ａ："Year, I'm pretty hungry." または "T!"



要するに、else の場合を「T」であらわすのである（Scheme だと #t になるが...）。上記の例だと次の通り。

(cond ((= a b) 0) /* a == b なら 値は 0 */ ((> a b) 1) /* a > b なら 値は 1 */ (T -1) /* その他（a < b） なら 値は -1 */ )

だから、listp, atomp, nullp の定義は次の通り。これも参考で、ライブラリ関数ではいちいち関数呼び出しをするまでもない。

Item TrueObj, FalseObj; Item listp( Item at ) { evaluate( at ); if( at == NULL ) { return FalseObj; } if( at->kind == ITEM_LIST ) { return TrueObj; } return FalseObj; } Item atomp( Item at ) { evaluate( at ); if( at == NULL ) { return FalseObj; } if( at->kind == ITEM_ATOM ) { return TrueObj; } return FalseObj; } Item nullp( Item at ) { evaluate( at ); if( at == NULL ) return TrueObj; } return FalseObj; }

だから、func_if() で使った isTrue() の定義は次のようになる。

int isTrue( Item at ) { if( at == TrueObj ) { return 1; } return 0; }

さあ、役者は揃った。Lisp の幕が上がるぞ！

簡単なプログラム

さて、以上が Lisp の役者たちである。アレ？ 繰り返し構造が何もないぞ？？？ という言い回しはいかにも解説臭いが、Lisp では繰り返し構造は「再帰」で表現するのがデフォルトである（一応、繰り返し構造を実現する関数である do だってあるんだが、恥ずかしいので省略）。これを今まで説明した関数で実現してみよう。まずやってみるのは数値アトムだけのリストの総和である。

(define (adds x) (if (nullp x) 0 (+ (car x) (adds (cdr x))) ) )

(define (adds x) というのは、関数を定義する時の書き方(Scheme 風)だと理解してくれれば良い。正確にはこれは (define adds (lambda (x)...) の省略形であり、関数本体が書かれた lambda 関数を評価した内容（関数定義自体を「閉包」として返す）を、adds というシンボルに束縛する（「代入」と同じような概念）ことを示す。この本体は単純な if に過ぎない。要するに、引数 x が NULL、つまりリストの末尾を越えた時には、0 を返し、そうでない場合には (+ (car x) (adds (cdr x))) を評価して値を返すのである。

ちなみに Common Lisp では (defun adds (x) ...) になる。微妙に違うのが嫌らしい。

問題は当然 (+ (car x) (adds (cdr x))) が何か、ということである。これの全体は２引数の数値加算 (+ A B) である。だから、 A = (car x) と、 B = (adds (cdr x)) を足したものを返すのである。前半のAは難しくない。単に引数xとして与えられたリストの先頭要素を取り出したもの(car) が A になるだけである。しかし B は、再帰である。つまり、今の引数 x に対して、先頭要素を除いた x(cdr)を、改めて自分自身の関数に対し引数で渡すのである。だから、実行は次のようになる。

> (adds '(1 2 3)) - function adds '(1 2 3) -- function adds '(2 3) --- function adds '(3) ----- function adds NULL ===== 0 → (if (nullp NULL) 0) == 0 === (+ 3 0) → (car (3)) == 3, (cdr (3)) == NULL, (adds NULL) == 0 == (+ 2 3) → (car (2 3)) == 2, (cdr (2 3)) == (3), (adds (3)) == 3 = (+ 1 5) → (car (1 2 3)) == 1,(cdr (1 2 3)) == (2 3),(adds '(2 3)) == 5 6

これはモロに再帰のパターンである。Ｃで先程のライブラリを使って書き直そう。

/* 足し算の下請け（２引数版。不定引数じゃ面倒だろ） */ Item add2( Item a1, Item a2 ) { Item ret; int added = getValue(a1) + getValue(a2); ret = newItemWithInt( added ); return ret; } Item adds( Item x ) { if( x == NULL ) { return newItemWithInt( 0 ); } else { return add2( car(x), adds( cdr(x) ) ); } }

まあ、この程度のものである。もし、再帰の例題にありがちな階乗を求めるプログラムだとこうなる。

(define (fact n) (if (= n 0) 1 (* n (fact (+ n -1))) ) )

これをＣに直せば次の通り。

Item fact( Item n ) { int val = getValue(n); if( val == 0 ) { return newItemWithInt( 1 ); } else { Item done = fact( newItemWithInt( val - 1 ) ); return newItemWithInt( val * getValue(done) ); } }

まあ、これじゃ Item を生成し倒してしまうので、効率はサイテーだ。しかし、普通の再帰プログラムでのやり方と比較して欲しい。

int fact( int val ) { if( val == 0 ) { return 1; } else { return val * fact( val - 1 ); } }

基本構造が同じであることは、明白であろう。

もう少し難しい関数

さて、Lisp の本質は別に数値を計算することにあるのではなくて、リスト操作にある。つまり、リストの検索・置換から、リストの結合や入れ子リストの平準化（入れ子リストを単純なリストに「平らにする」）、リストの逆転など、面白い操作を定義していくことができる。これらを再帰を使って記述していこう。

まず、その前にリスト構造を表示するプログラムを書いてみる。これは結果表示に必須だな。

#define nullp(p) (p)?(0):(1) #define listp(p) ((p)->kind==ITEM_LIST)?(1):(0) #define atomp(p) ((p)->kind==ITEM_ATOM)?(1):(0) void showItems( Item at ) { if( listp(at) ) { while( ! nullp(at) ) { Item child = car(at); if( listp(child) ) { printf( "( " ); showItems( child ); printf( " ) " ); } else if( nullp(child) ) { printf( "NIL " ); } else { /* ATOM */ printf( "%s ", child->data ); } at = cdr(at); } } else { printf( "%s ", at->data ); } } void show( Item at ) { if( nullp(at) ) { printf( "NIL

" ); return; } if( atomp(at) ) { printf( "%s

", at->data ); return; } printf( "( " ); showItems( at ); printf( ")

" ); }

まあ、空白の入れ方が Lisp の表示と比較して気持悪いというのがあるが、凝りたければどうぞ頑張ってくれ。とりあえず目的はこれでも果たせる。

search

ではまず、フラットなリストに対して、特定の値があればそれを返し、なければ NIL を返す関数を書いてみよう。Lisp だと次のようなものである。

(define (search a x) (cond ((nullp x) NIL) → これが停止条件になる！！！ ((= (car x) a) (car x)) → 見つかった場合は素直に返す (T (search a (cdr x))) → 次を見て行く ) ) search > (search 5 '(1 2 4 5 2)) 5 > (search 2 '(1 2 4 5 2)) 2 > (search 3 '(1 2 4 5 2)) NIL

素直に Lisp のソースを睨んで実装する。

/* 比較の下請け関数 */ int isEqual( Item a, Item b ) { if( a == NULL && b == NULL ) { return 1; } else if( a == NULL || b == NULL ) { return 0; } if( a->kind != ITEM_ATOM || b->kind != ITEM_ATOM ) { warning( "isEqual compares only ATOMS

" ); /* NOTREACHED */ } if( strcmp( (char *)a->data, (char *)b->data ) == 0 ) { return 1; } return 0; } Item search( Item atm, Item lis ) { if( ! atomp(atm) ) { warning( "search: atm must be LIST

" ); /* NOTREACHED */ } if( nullp(lis) ) { return NULL; } else if( isEqual( atm, car(lis) ) ) { return car(lis); } else { search( atm, cdr(lis) ); } }

しかし、複数の項目が見つかる場合には、今のコードでは最初のものしか見つけない。これをリストで返すことにしよう。そうすると、Lisp のコードでは次のようになる。

(define (search a x) (cond ((nullp x) NIL) ((= (car x) a) (cons (car x) (search a (cdr x)))) (T (search a (cdr x))) ) ) search > (search 5 '(1 2 4 5 2)) ( 5 ) > (search 2 '(1 2 4 5 2)) ( 2 2 ) > (search 3 '(1 2 4 5 2)) NIL

では、赤字強調の部分をＣの方も直すことにする。

} else if( isEqual( atm, car(lis) ) ) { return cons( car(lis), search( atm, cdr(lis) ) ); } else { search( atm, cdr(lis) );

Lisp のソースとＣのライブラリのソースとが、きっちりと対応していることがお分かりかな？ つまり、cons を使って、新しいリストを返しているのである。しかし、ここで注意されたいのは、そのリストに繋がっているアトムのデータは一切コピーされていない、ということである。つまり、cons は次のように新しいリストを作成するが、引数を単にその新しいリストにセットしているだけである。

Item cons( Item a1, Item a2 ) { Item node; node = newItem(); node->kind = ITEM_LIST; node->data = a1; node->next = a2; return node; }

append

この話を良く理解するために、リストの連結操作を考えてみよう。つまり、２つのリストを渡されて、それらを結合したリストを返す関数である。Lisp の標準組み込み関数には、replaca, replacd, nconc（Common Lisp流） とか set-cdr!, set-car!（Scheme 流） のような「破壊的なリスト操作」をする関数がある。しかし、これらは副作用を持つので、「正統的な Lisp らしい Lisp」の操作とは毛色が違う。たとえば、次のような操作を見てみよう。

> (define x '(1 2 3)) > (define y '(4 5 6)) > (append x y) (1 2 3 4 5 6) > x (1 2 3) > y (4 5 6)

つまり、非破壊的リスト連結関数 append の結果は、リストの連結であるが、それは副作用を持たず、変数 x, y の既存の内容を破壊しない。ところが破壊的なリスト操作をする関数を使ったものでは次のようになる。

> (desctractive-append x y) (1 2 3 4 5 6) > x (1 2 3 4 5 6) > y (4 5 6)

Lisp はいわゆる「関数型プログラミング言語」である。この「関数型」という言葉は、Ｃ言語が「手続き」を「関数」として実装している、というような意味ではない。そうではなくて、原理的に「関数」は副作用を持たず、他のオブジェクトを変更しない、という意味なのである。そういう意味では「破壊的なリスト操作」は異端であり、理論上も美しくない。だから、非破壊的リスト連結関数 append は次のようにして実装する（まあ、効率を考えてフツーは組み込み関数として用意されてるけど...）。

(define (append a b) (if (nullp a) b (cons (car a) (append (cdr a) b)) ) )

この時、第１引数のリストはすべてその Item のリスト部分は cons によってコピーされて、最後の next == NULL に相当する部分だけが、第二引数を示すことになる。だから、これによって非破壊的にリストを連結できるのである。しかし、コピーは cons によるものなので、実際のアトムとしてのデータは一切コピーされずに、オリジナルの引数と同じものを指しているのである。これをＣで実装する。

Item append( Item a, Item b ) { if( nullp(a) ) { return b; } else { return cons( car(a), append( cdr(a), b ) ); } }

flat

では、この append を使って、リストの平準化を行う。つまり、入れ子になった複雑なリストを、単純な一次元のリストに「平たく」するのである。つまり、次の通り。

> (flat '(1 2 (3 (4 5)) 6 (((7))))) (1 2 3 4 5 6 7)

これは Lisp では次のように書けば良い。

(define (flat x) (cond ((nullp x) NIL) ((atomp x) x) ((atomp (car x)) (cons (car x) (flat (cdr x)))) (T (append (flat (car x)) (flat (cdr x)))) ) )

さて、複雑になってきたぞ！！ これに取り掛かるためには、順番というものもあるのだよ。以下、順を追ってどう書くか解説していこう。

まず、「flat は引数として与えたリストを平たくする！」という呪文を３回繰り返す。これによってあなたに魔法がかかる。

→ (define (flat x) .... )



まず停止条件を考える。「停止条件」と言っても、そんなに固くならなくても良い。要するに引数が一番「バカな」場合を考えるのである。今回の場合は、引数が NIL のケース、引数がアトムのケースを考えておけば良い。以降引数の場合分けを考えていく。

→ (cond .....)



引数が NIL ならば、単に戻り値は NIL で十分だ。そりゃ、そうだ。深く考えても仕方がない。

→ ((nullp x) NIL)



引数がアトムの場合、これはすでに「平たく」なっているものと考えてしまおう。だから引数のアトムをそのまま返す。以上の２つが停止条件になる。

→ ((atomp x) x)



それじゃ、残りの場合は引数がリストであるケースだ。一般に Lisp のリスト処理は、最初の要素とそれ以降の要素をバラして考えるのが定石である。だから (car x) と (cdr x) にバラして考えていく。このうち、最初以外の以降の要素である (cdr x) の処理については、あなたに充分魔法がかかっていれば、「ん！ そりゃやっぱり flat に任せりゃいいじゃん！」と気がつくだろう。もしそう思えれなかったら、あなたにはまだ充分魔法がかかっていない。もう一度、最初の呪文を唱え直すべきである！

→ (flat (cdr x))



では、 (car x) について考えていこう。 (car x) は２通りの可能性がある。つまり、アトムである場合と、リストである場合である。もし、アトムならば、単純に (flat (cdr x)) で帰ってくる、「平たく」なったリストに対して、 cons で先頭に追加してやれば良いだけである。だから次のようにすれば良いだけだ。

→ ((atomp (car x)) (cons (car x) (flat (cdr x))))



さて、 (car x) がリストである場合（cond で「それ以外」の場合）である。 (flat (cdr x)) は「平たく」なったリストを返して来るのだが、それに対して (car x) を平たくして、リストとして連結してやれば良い。もしあなたに充分魔法がかかっているのなら、「ん、これもやっぱり flat に任せりゃいいじゃん！」とあなたは考えるだろう。そうでなければ、あなたは呪文の効き目が弱い体質なのだろう。最初の呪文を１０回ほど唱え直すべきだ。だから、 (flat (car x)) と (flat (cdr x)) とを連結すれば良い、ということになる。これには先程作った append を使う（フツーは組み込み関数として用意されてるが...）。

→ (T (append (flat (car x)) (flat (cdr x))))



めでたし、めでたし。





だから、ここで「flat って今定義してるんだから...使って大丈夫？？」とか「あれ、今何やってんだっけ？」と思ったら、確実に地獄に落ちる。再帰の最大のコツは、「自分が書いたプログラム（の機能定義）を信じること」なのである。ここで一発ジョーク。「最後の審判は再帰によって判定される。なぜならば信仰が足りないものは、インフェルノに落されるのだから...」。要するにプログラマとしての自我を賭けて、自分の書いた機能仕様を信じることが特に面倒な再帰を理解するコツなのである。上記の場合分けは熟読の価値があるぞ！！！ ぜひ頑張って理解し「悟り」開いて頂きたい。

reverse

さて、次はリストの逆順である。これはちょっとした小技を使う。

(define (reverse x) (rev x NIL) )

という風に、１引数のインターフェイスから、２引数の下請け関数に仕事を出すのである。２引数の rev の定義は次の通り。

(define (rev a b) (if (nullp a) b (rev (cdr a) (cons (car a) b)) ) )

これは言ってみればスタックの技である。つまり、第２引数に第１引数を頭から順に積んで行くと、結果として第２引数に第１引数が逆順に構築される。そして、第１引数が尽きた時に、関数の値として第２引数を返す、ということをしているに過ぎない。もうこの程度の再帰ならば、明白に理解できることであろう。当然、Ｃで書けば次の通りである。

Item rev( Item a, Item b ) { if( a == NULL ) { return b; } else { return rev( cdr(a), cons( car(a), b ) ); } } Item reverse( Item x ) { rev( x, NULL ); }

もう、Ｃと Lisp の対応関係なぞ、自明であろう。

qsort

ちなみにクィックソートだと、次の通り。Ｃで書いたものよりも、随分と判りやすいことに気づくだろう。

/* クイックソートのドライバ関数 */ (define (qsort x) (cond ((nullp x) NIL) /* 自明な場合.. NIL */ ((nullp (cdr x)) x) /* 自明な場合.. 要素１つ */ (T (qsort2 (car x) (cdr x) NIL NIL)) ) ) /* 実作業を担当する下請け a = ピボット x = ピボット以降のリスト L = 小さい側の結果 R=大きい側の結果 */ (define (qsort2 a x L R) (cond ((nullp x) /* とりあえず、左右に振り分けたから、R, L それぞれをまたソート */ (append (qsort R) (cons a (qsort L)))) /* 左右への振り分け */ ((< (car x) a) /* xの先頭 < ピボット なので、xの先頭は左側へ cons */ (qsort2 a (cdr x) (cons (car x) L) R) ) (T /* xの先頭 >= ピボット なので、xの先頭は右側へ cons */ (qsort2 a (cdr x) L (cons (car x) R))) ) )

筆者に言わせれば、このクィックソート・プログラムは手続き型言語のものよりも１００倍判りやすいプログラムであると思うよ。

インタプリタ・コンパイラと「構文木」

実はこのような子持ちの複雑なリスト構造というのは、コンパイラやインタプリタでも重要な役割を果たしている。例えば、次のようなＣ言語の関数を処理するには、lex と yacc を使った構文解析処理が行われるのだが、その直接の出力は「構文木」と呼ばれるリストである。

char *strncpy( char *dst, char *src, int n ) { int i; for( i = 0; i < n; i++, src++ ) { *dst++ = *src; if( *src == '\0' ) { break; } } }

たとえば....

((char *) strncpy ( ((char *) dest) ((char *) src) ((int) n) ) ((int) i) (for ((= i 0) (< i n) ((++ i) (++ src))) ((= (++ (* dst)) (* src)) (if (== (* src) '\0') break) ) ) )

つまり、リスト構造に直すことによって、その演算子の優先順位を明確にし、構文的な構造と入れ子関係を明白にし、Ｃ言語プログラムを扱いやすいデータ構造に変形するのである。このような「構文木」を元にして、そのプログラムが意味するものと同一の機械語プログラムを作成するのがコンパイラであるし、インタプリタならば、これをそのまま Lisp のように実行してやれば良いのである（勿論作るのは大変だが）。例のＧＮＵのリチャード・ストールマンは、実は Lisper 出身である（だから emacs のマクロ言語が elisp なのさ！）。そのため、現在デフォルトのコンパイラである gcc は内部で、Ｃ言語のプログラムを「RTL」と呼ぶ Lisp 風言語に変換する。そして、その「RTL」のプログラム（この時点ではデータであり同時にプログラムである）を、パターンマッチングの技法を使ってターゲットＣＰＵごとに最適なコードを生成する、という技を使っている（ソースの gcc/config/*/*.md という記述を参照）。たとえば、egcs-1.1.2/gcc/config/i386/i386.md というファイルの中には次のような記述がある。

;; On a 386, it is faster to push MEM directly. (define_insn "" [(set (match_operand:SI 0 "push_operand" "= これは Intel 386 ＣＰＵの特性として、メモリ内容を直接スタックに PUSH することができる、という性質を利用することを記述したものである。つまり、Lisp 風のデータ兼プログラムとして、define_insn で定義されるフォーマットは大体次のようである。 (define_insn "RTL仮想機械の命令" [RTLで操作の意味を記述] "最終条件" "* 出力コード" ) という具合である。だから、Ｃ言語ソースがRTLに変換されて、それが [RTLで操作の意味を記述] の定義を満たす時に "* 出力コード" に変換されてターゲット用のアセンブリコードが生成されるのである。このような変換ルールが、個別ＣＰＵごとにRTLのかたちで存在し、これを使って最適なコードを生成しているのである。 では具体的なＣ言語プログラムから生成された RTL はどうすれば見えるのだろう？ これは gcc のコンパイルオプションを使う。man ページなどで見ると、次のようなオプションがある(DEBUGGING OPTIONS の項目)。やたらと多いぞ... しかし、オプションの順番が処理の順番に対応しているので、コンパイラがどういう流れでコンパイルをしているのかが一目瞭然である。これはホントに勉強になるぞ！（ただし、man はやや古い。info の 方が正確である。） -dr RTL生成部の後のダンプを `file.rtl' へ出力。 -dj 第１ジャンプ最適化の後のダンプを file.jump' へ出力。 -ds 共通部分式削除部(CSE)の後のダンプを `file.cse' へ出力。 -dG 大域共通部分式削除部の後のダンプを `file.gcse' へ出力。 -dL ループ最適化の後のダンプを `file.loop' へ出力。 -dt 第２共通部分式削除部(CSE)の後のダンプを `file.cse2' へ出力。 -df フロー解析の後のダンプを `file.flow' へ出力。 -dc 命令組合せ部の後のダンプを `file.combine' へ出力。 -dS 第１命令スケジューリングの後のダンプを `file.sched' へ出力。 -dl 局所レジスタ割当部の後のダンプを `file.lreg' へ出力。 -dg 大域レジスタ割当部の後のダンプを `file.greg' へ出力。 -dR 第２命令スケジューリングの後のダンプを `file.sched2' へ出力。 -dJ 第２ジャンプ最適化の後のダンプを `file.jump2' へ出力。 -dd 遅延分岐スケジューリングの後のダンプを `file.dbr' へ出力。 -dk 数値演算スタック最適化の後のダンプを `file.stack' へ出力。 -da 以上の中間形式をすべてファイルに出力。 では、各最適化処理について、簡単に info からまとめよう。ここらへんの用語について詳しく知りたい読者はぜひAho,Sethi,Ullmanの「コンパイラ」を読みたまえ。いわゆる「Dragon Book」であり、ハッカーの必読書の一つである。 構文解析部が、Ｃプログラムを RTL に変換する。(-dr, *.rtl) 第１ジャンプ最適化。複雑な多重ジャンプを単純化する。この処理は RTL への変換の直後、共通部分式削除部の後、最終出力の直前と３回行われうる。このジャンプ最適化の結果として、非到達文が検出されて削除されることがある。(-dj, *.jump) 第１共通部分式削除。条件ジャンプを厳密に判定し、可能ならば非条件ジャンプに変換する。(-ds, *.cse) 大域共通部分式削除。グローバルな定数について重複部分を削除する。(-dG, *.gcse) ループ最適化。ループ内で値の変わらない処理を括り出して、それをループ外に移動する。(-dL, *.loop) 第２共通部分式削除。ループ最適化の後に、もう一度共通部分式削除をやりなおす。(-dt, *.cse2) フロー解析。変数の「使われ方」を検討し、値が使われない変数への値のセットを積極的に削除する。その結果、実質的に使われない自動変数（や使われない関数戻り値）が削除される。(-df, *.flow) 命令組合せ部。複数の命令によって構成される命令について、より良い単純な命令があればそれに置き換える。(-dc, *.combine) 第１命令スケジューリング。命令の順番を入れ換えることで、メモリなどの利用効率が上がることがあるので、このような順番の入れ換えを試みる。(-dS, *.sched) 局所レジスタ割当。基本処理ブロックの範囲内で、使われる変数を実際のＣＰＵのレジスタに割り当てる。(-dl, *.lreg) 大域レジスタ割当。基本処理ブロックの範囲を越えて、使われる変数を実際のＣＰＵレジスタに割り当てる。(-dg, *.greg) 第２命令スケジューリング。順番の入れ換えを割り当てられたレジスタに関して可能であるか試みる。(-dR, *.sched2) 第２ジャンプ最適化。多重ジャンプを単純なジャンプに変換する。(-dJ, *.jump2) 遅延分岐スケジューリング。ジャンプや関数コールの順番を入れ換えることで、より最適化ができないかどうか検討する。(-dd, *.dbr) 数値演算スタック最適化。これは Intel 80387 コプロセッサ専用。数値演算コプロセッサが利用するスタックを最適化する。(-dk, *.stack) ＣＰＵ依存の「覗き穴最適化」の後、アセンブリを出力する。 だから、適当なプログラムを -da でコンパイルしてみよう。そうすると、大量のファイルが生成される。その最適化処理が最適化オプションによってスキップされていれば、中身は空なので、-O0（最適化せず）、-O1（ある程度最適化する）、-O2（しっかり最適化する）の最適化オプションを変えてやってみると、次のようになる。

最適化 -O0 -O1 -O2 RTL生成部 *.rtl ○ ○ ○ 第１ジャンプ最適化 file.jump ○ ○ ○ 共通部分式削除部(CSE) *.cse × ○ ○ 大域共通部分式削除 *.gcse × × ○ ループ最適化 *.loop × ○ ○ 第２共通部分式削除部(CSE) *.cse2 × ○ ○ フロー解析 *.flow ○ ○ ○ 命令組合せ部 *.combine ○ ○ ○ 第１命令スケジューリング・パス *.sched × × × 局所レジスタ割当部 *.lreg ○ ○ ○ 大域レジスタ割当部 *.greg ○ ○ ○ 第２命令スケジューリング・パス *.sched2 × × ○ 第２ジャンプ最適化 *.jump2 × ○ ○ 遅延分岐スケジューリング *.dbr × ○ ○ 数値演算スタック最適化 *.stack ○ ○ ○

たとえば、次のＣ言語のコード(simple.c)は RTL(simple.c.rtl) では次のようになる。

int fact( int n ) { if( n <= 0 ) return 1; return n * fact( n - 1 ); } 長いのでループ内部のみ (code_label 14 13 17 2 "") (insn 17 14 19 (set (reg:SI 23) (plus:SI (mem:SI (reg:SI 17)) (const_int -1))) -1 (nil) (nil)) (insn 19 17 21 (set (mem:SI (pre_dec:SI (reg:SI 7 %esp))) (reg:SI 23)) -1 (nil) (nil)) (call_insn 21 19 23 (set (reg:SI 0 %eax) (call (mem:QI (symbol_ref:SI ("fact"))) ←再帰呼び出し (const_int 4))) -1 (nil) (nil) (nil)) (insn 23 21 25 (set (reg:SI 7 %esp) (plus:SI (reg:SI 7 %esp) (const_int 4))) -1 (nil) (nil)) (insn 25 23 26 (set (reg:SI 24) (reg:SI 0 %eax)) -1 (nil) (nil)) (insn 26 25 28 (set (reg:SI 22) (mult:SI (reg:SI 24) (mem:SI (reg:SI 17)))) -1 (nil) (nil)) (insn 28 26 29 (set (reg/i:SI 0 %eax) (reg:SI 22)) -1 (nil) (nil)) (insn 29 28 30 (use (reg/i:SI 0 %eax)) -1 (nil) (nil)) (jump_insn 30 29 31 (set (pc) (label_ref 35)) -1 (nil) (nil))

見るからに Lisp 風の RTL に変換されているのである。これを最適化すると次のような RTL に変換するのである(simple.c.stack)。ここでは具体的なレジスタ(%esp,%ebpなど)に割り当てられていることに注意されたい。あとはこれを使って具体的なアセンブリに変換し、それをアセンブラが機械語に変換するだけである。

(insn 19 25 21 (set (reg:SI 0 %eax) (plus:SI (reg/v:SI 3 %ebx) (const_int -1))) 143 {addsi3+1} (nil) (nil)) (insn 21 19 23 (set (mem:SI (pre_dec:SI (reg:SI 7 %esp))) (reg:SI 0 %eax)) 50 {movsi-2} (insn_list 19 (nil)) (expr_list:REG_DEAD (reg:SI 0 %eax) (nil))) (call_insn 23 21 26 (set (reg:SI 0 %eax) (call (mem:QI (symbol_ref:SI ("fact"))) ←再帰呼び出し (const_int 4))) 333 {call_value+1} (insn_list:REG_DEP_ANTI 19 (insn_list:REG_DEP_ANTI 21 (nil))) (nil) (nil)) (insn 26 23 28 (set (reg/v:SI 3 %ebx) (mult:SI (reg/v:SI 3 %ebx) (reg:SI 0 %eax))) 164 {mulsi3} (insn_list 23 (insn_list:REG_DEP_ANTI 19 (nil))) (expr_list:REG_DEAD (reg:SI 0 %eax) (nil))) (insn 28 26 30 (set (reg/i:SI 0 %eax) (reg/v:SI 3 %ebx)) 54 {movsi+2} (insn_list:REG_DEP_ANTI 23 (insn_list 26 (nil))) (expr_list:REG_DEAD (reg/v:SI 3 %ebx) (nil))) (jump_insn 30 28 15 (set (pc) (label_ref 37)) 309 {jump} (insn_list:REG_DEP_ANTI 28 (nil)) (nil))

このように Lisp 風の処理は、コンパイラのような特に高度なプログラムの根底の部分で広く活躍しているのである。

再帰の弱点とその克服

再帰はこのように、非常にオモシロく、重要なロジックである。再帰の方が、少なくともハッカーには判りやすく、しかもメンテしやすいコードになる場合も多い。先程のクィックソートなどは良い例だ。なぜならば、「現在の状態」から「次の処理」を導き出し、適切な終了条件を設定する、というのが再帰の本質であり、これは実質上「数学的帰納法」なのである。数学的帰納法は数論の基本的な証明手段であり、当然数学では「証明」のために使われるが、これをプログラミングに応用したのが他ならぬ「再帰」であると言っても良い。だから、「計算論」では「再帰」こそがもっとも基本的なプログラミング手段であり、数学センスに溢れた手法なのである（余談：ゲーデルの「不完全性定理」の証明でも、「再帰」を使って四則演算から自動証明のための関数を導き出して証明している。「再帰」は、数学的にはプログラミングの「基礎」の「基礎」なのである）。

しかし、再帰には重大な弱点がある。それはスタックを浪費することである。再帰で呼ばれるたびに、スタックには引数と戻りアドレスが積まれ、ローカル変数が確保される。そのため、再帰が深くなればなるほど、スタックが浪費されるのである。これは MS-DOS(Intel 8086) のようなスタックに限りのある（それもかなり小さい）システムでは重大な問題だった。

昔話。MS-DOS(Intel 8086) では、セグメント・アーキテクチャと称して、16bit のレジスタによるアドレッシングを基本としていた。そのため、スタックも実質上 16bit で表現可能な 65536byte（32768 word） の上限があったのである。これは大変小さいサイズである。更にデフォルトのメモリモデルであるスモールモデルでは、データセグメントとスタックセグメントを共用していた。だから、リンカオプションで「どれだけのスタックセグメントを確保するか？」という指定が出来、再帰を使うプログラムでは実質上必須（あるいはデータセグメントとスタックセグメントを別セグメントを使うように指定する！）だったのである。そのため、実質上 MS-DOS(Intel 8086)では、再帰プログラムが使いものにならないという現実があったのである。悲しい過去があるのだ....

しかし、計算論の「再帰定理」と呼ばれる定理では、再帰プログラムが必ず再帰ではないループに書き換えることができる、ということを証明している。このため、コンパイラでは再帰のプログラムを自動的にループに書き換えることができるとウレシイ。実際、gcc のような賢いコンパイラでは、「末端再帰の最適化」と呼ばれる最適化オプションが存在する。これはつまり、一部の再帰関数をループ構造にコンパイラが書き換えて扱う、というものである。

けど、再帰定理は「存在する！」ことを証明してくれているだけだよ...一般の場合にどう変換するのか、までは再帰定理の担当ではない。ここらへんが奥深い

「末端再帰関数」とは、再帰関数であるが自分自身を呼び出すだけの単純な再帰関数のことである。今まで紹介して来た再帰関数はすべてこの部類である。勿論、再帰関数として２つの関数が互いに相手を呼び合う、というタイプのものも考えることが出来るし、それらを「再帰定理」の結果としてループ構造に書き換えることも出来るのだが、さすがにこれは重要性が低く、しかもちょっとだけ難しい（ちゃんとフロー解析をして、２つの関数を１つにまとめ上げて「末端再帰の最適化」をする、というあたりの処理が必要）。だから、一番メリットがあり、間違う余地も少ない「末端再帰関数」だけをループ構造に直すオプションが gcc では追加されている。（ただし、Intel386は未実装であり残念。）

これはたとえば次のようになる。リストを逆転する関数 reverse() の下請けである rev() だと、こんなところである。

Item rev( Item a, Item b ) { if( a == NULL ) { return b; } else { return rev( cdr(a), cons( car(a), b ) ); } } /* 末端再帰を最適化する！ */ Item rev( Item a, Item b ) { while( 1 ) { if( a == NULL ) { return b; } else { Item ta, tb; /* 引数渡しが相互に影響するのを防ぐ */ ta = cdr(a); tb = cons( car(a), b ); a = ta; b = tb; /* これで正しく引数が渡る */ } } }

また、複雑な再帰プログラムだと、再帰プログラムが使うスタックセグメントを、自前のスタックデータ構造で模倣した方がやりやすい。この例は「スタック：再帰構造の回避〜ディレクトリの再帰ファイルリスト」にある。

copyright by K.Sugiura, 1996-2006