whileループが本質的に再帰かどうか疑問に思いましたか?
なぜなら、whileループは最後に自分自身を呼び出す関数とみなすことができるからだと思います。再帰ではない場合、違いは何ですか?
whileループが本質的に再帰かどうか疑問に思いましたか?
なぜなら、whileループは最後に自分自身を呼び出す関数とみなすことができるからだと思います。再帰ではない場合、違いは何ですか?
回答:
ループは再帰ではありません。実際、それらは反対のメカニズムの最も重要な例です:繰り返し。
再帰のポイントは、処理の1つの要素がそれ自体の別のインスタンスを呼び出すことです。ループ制御機構は、単に開始点にジャンプして戻ります。
コード内をジャンプして、別のコードブロックを呼び出すことは、異なる操作です。たとえば、ループの先頭にジャンプしても、ループ制御変数の値はジャンプ前と同じままです。ただし、現在のルーチンの別のインスタンスを呼び出すと、新しいインスタンスには、そのすべての変数の新しい無関係なコピーが含まれます。事実上、1つの変数は、処理の最初のレベルで1つの値を持つことができ、より低いレベルで別の値を持つことができます。
この機能は、多くの再帰アルゴリズムが機能するために重要です。これが、すべての値を追跡する呼び出されたフレームのスタックを管理せずに、反復を介して再帰をエミュレートできない理由です。
Xが本質的にYであると言うのは、Xを表現していることを念頭に置いた(正式な)システムがある場合にのみ意味がありますwhile
。レジスタマシンの観点から定義する場合、おそらく定義しません。
どちらの場合でも、whileループが含まれているという理由だけで関数を再帰的に呼び出すと、人々はおそらくあなたを理解しないでしょう。
*おそらく間接的にのみですが、たとえばを使用して定義する場合fold
。
while
コンストラクトを持つ言語では、再帰性は一般に関数のプロパティであるため、このコンテキストで「再帰的」と表現するものは考えられません。
これはあなたの視点に依存します。
計算可能性理論を見ると、反復と再帰は等しく表現力があります。これが意味することは、何かを計算する関数を書くことができるということであり、それを再帰的または反復的に行うかどうかは関係なく、両方のアプローチを選択することができます。反復的に計算できない再帰的に計算できるものはありません(逆も同様です)(プログラムの内部動作は異なる場合があります)。
多くのプログラミング言語では、再帰と反復を同じように扱わず、正当な理由があります。通常、再帰とは、言語/コンパイラが呼び出しスタックを処理することを意味し、反復とは、自分でスタック処理を行う必要がある場合があることを意味します。
ただし、ループ(for、while)のようなものが実際には再帰の構文糖のみであり、そのように舞台裏で実装される言語、特に関数型言語があります。関数型言語ではこれが望ましい場合が多くあります。通常、ループは他の方法ではループの概念を持たないため、実用的な理由はほとんどありませんが、ループを追加すると計算が複雑になります。
いいえ、本質的に同じではありません。それらは同じように表現力があり、繰り返し計算することはできませんし、再帰的に計算することはできませんが、一般的な場合はそれについてです(教会チューリングの論文による)。
ここで再帰プログラムについて話していることに注意してください。データ構造(ツリーなど)には、再帰の他の形式があります。
あなたからそれを見れば実装の観点、そして再帰と反復はほとんど同じではありません。再帰は、呼び出しごとに新しいスタックフレームを作成します。再帰のすべてのステップは自己完結型であり、呼び出し先(それ自体)から計算の引数を取得します。
一方、ループは呼び出しフレームを作成しません。彼らにとって、コンテキストは各ステップで保持されません。ループの場合、プログラムはループ条件が失敗するまでループの開始点にジャンプして戻るだけです。
これは、実世界でかなり根本的な違いを生む可能性があるため、知ることが非常に重要です。再帰では、呼び出しごとにコンテキスト全体を保存する必要があります。反復では、メモリ内の変数と保存場所を正確に制御できます。
そのように見ると、ほとんどの言語で反復と再帰が根本的に異なり、プロパティが異なることがすぐにわかります。状況によっては、一部のプロパティが他のプロパティよりも望ましい場合があります。
再帰を使用すると、プログラムをよりシンプルで簡単にテストおよび証明できます。通常、再帰を反復に変換すると、コードがより複雑になり、失敗の可能性が高くなります。一方、反復に変換して呼び出しスタックフレームの量を減らすと、必要なメモリを大幅に節約できます。
違いは、暗黙的なスタックとセマンティックです。
「最後に自分自身を呼び出す」whileループには、完了時にクロールするスタックがありません。最後の反復で、終了時の状態を設定します。
ただし、以前に行われた作業の状態を記憶するこの暗黙的なスタックなしでは、再帰を行うことはできません。
スタックへのアクセスを明示的に許可した場合、反復で再帰問題を解決できることは事実です。しかし、そのようにするのは同じではありません。
意味の違いは、再帰コードを見ると、反復コードとはまったく異なる方法でアイデアが伝わるという事実に関係しています。反復コードは、一度に1ステップずつ処理を行います。以前の状態を受け入れ、次の状態を作成するためにのみ機能します。
再帰コードは問題をフラクタルに分割します。この小さな部分はその大きな部分のように見えるので、この少しだけでも同じようにできます。問題について考えるのは別の方法です。それは非常に強力で、慣れるのに時間がかかります。多くは数行で言えます。スタックにアクセスできる場合でも、whileループからそれを取得することはできません。
それはすべて、用語の本質的な使用にかかっています。プログラミング言語レベルでは、構文と意味が異なり、パフォーマンスとメモリ使用量がまったく異なります。しかし、理論を深く掘り下げると、互いの観点から定義できるため、理論的な意味で「同じ」になります。
本当の質問は次のとおりです。反復(ループ)と再帰を区別するのはいつ意味がありますか。また、いつそれを同じものと考えるのが便利ですか。答えは、(数学的な証明を書くのではなく)実際にプログラミングするときは、反復と再帰を区別することが重要だということです。
再帰は、新しいスタックフレーム、つまり、呼び出しごとにローカル変数の新しいセットを作成します。これにはオーバーヘッドがあり、スタック上のスペースを占有します。つまり、十分な深さの再帰がスタックをオーバーフローさせ、プログラムがクラッシュする可能性があります。一方、反復は既存の変数を変更するだけなので、一般に高速であり、一定量のメモリしか消費しません。したがって、これは開発者にとって非常に重要な違いです!
末尾呼び出し再帰を使用する言語(通常は関数型言語)では、コンパイラは、一定量のメモリのみを使用するように再帰呼び出しを最適化できる場合があります。これらの言語では、重要な区別は反復と再帰ではなく、非末尾呼び出し再帰バージョンの末尾呼び出し再帰と繰り返しです。
結論:違いを伝えることができなければ、プログラムがクラッシュします。
while
ループは再帰の一種です。たとえば、この質問に対する受け入れられた答えを参照してください。それらは、計算可能性理論のμ演算子に対応します(たとえば、こちらを参照)。
for
数値の範囲、有限コレクション、配列などで反復するループのすべてのバリエーションは、プリミティブな再帰に対応します。たとえばhereおよびhereを参照してください。そのノートfor
C、C ++、Java(登録商標)のループなどが、実際のための糖衣構文であるwhile
ループ、従ってそれはプリミティブ再帰に対応していません。Pascal for
ループは、プリミティブな再帰の例です。
重要な違いは、プリミティブな再帰は常に終了するのに対して、一般化された再帰(while
ループ)は終了しない可能性があることです。
編集
コメントおよびその他の回答に関するいくつかの説明。「事物がそれ自体またはそのタイプに関して定義されると、再帰が発生します。」(ウィキペディアを参照)。そう、
whileループは本質的に再帰ですか?
while
ループ自体を定義できるので
while p do c := if p then (c; while p do c))
そう、はい、while
ループは再帰の形式です。再帰関数は、再帰の別の形式です(再帰定義の別の例)。リストとツリーは、再帰の別の形式です。
多くの回答とコメントで暗黙的に想定されている別の質問は
whileループと再帰関数は同等ですか?
この質問に対する答えは「いいえ」です。while
ループは末尾再帰関数に対応します。ループによってアクセスされる変数は、暗黙の再帰関数の引数に対応しますが、他の人が指摘したように、非末尾再帰関数while
追加のスタックを使用せずにループでモデル化することはできません。
したがって、「while
ループは再帰の一種」であるという事実は、「一部の再帰関数はwhile
ループで表現できない」という事実と矛盾しません。
FOR
結局のところ、ループだけの言語は、すべてのプリミティブな再帰関数をWHILE
正確に計算でき、ループだけの言語は、すべてのµ再帰関数を正確に計算できます(そして、µ再帰関数は、チューリングマシンで計算できます)。または、短くするために:原始再帰とμ再帰は、数学/計算可能性理論の技術用語です。
再帰と無制限のwhileループの両方が計算表現力の点で同等であることは事実です。つまり、再帰的に記述されたプログラムは、代わりにループを使用して同等のプログラムに書き換えることができ、逆も同様です。どちらのアプローチもチューリング完全であり、いずれかの計算可能な関数を計算するために使用できます。
プログラミングに関する基本的な違いは、再帰により、呼び出しスタックに格納されるデータを利用できることです。これを説明するために、ループまたは再帰のいずれかを使用して、一重リンクリストの要素を印刷するとします。サンプルコードにはCを使用します。
typedef struct List List;
struct List
{
List* next;
int element;
};
void print_list_loop(List* l)
{
List* it = l;
while(it != NULL)
{
printf("Element: %d\n", it->element);
it = it->next;
}
}
void print_list_rec(List* l)
{
if(l == NULL) return;
printf("Element: %d\n", l->element);
print_list_rec(l->next);
}
シンプルでしょ?次に、少し変更を加えます。リストを逆順に印刷します。
再帰的なバリアントの場合、これは元の関数に対するほとんど些細な変更です。
void print_list_reverse_rec(List* l)
{
if (l == NULL) return;
print_list_reverse_rec(l->next);
printf("Element: %d\n", l->element);
}
ただし、ループ機能には問題があります。私たちのリストは一重にリンクされているため、前方にのみ移動できます。ただし、逆方向に印刷するため、最後の要素の印刷を開始する必要があります。最後の要素に到達すると、2番目から2番目の要素に戻ることはできなくなります。
そのため、多くの再走査を行うか、訪問した要素を追跡して効率的に印刷できる補助データ構造を構築する必要があります。
なぜ再帰にこの問題がないのですか?再帰では、補助データ構造がすでにあるため、関数呼び出しスタックです。
再帰により、以前の再帰呼び出しの呼び出しに戻ることができ、その呼び出しのすべてのローカル変数と状態はそのままであるため、反復的なケースでモデル化するのが面倒な柔軟性が得られます。
ループは、特定のタスク(主に反復)を達成するための特別な形式の再帰です。いくつかの言語で同じパフォーマンスの再帰スタイルでループを実装できます[1]。また、SICP [2]では、forループが「シンタスティックシュガー」として記述されていることがわかります。ほとんどの命令型プログラミング言語では、forおよびwhileブロックは親関数と同じスコープを使用しています。それにもかかわらず、ほとんどの関数型プログラミング言語には、ループが必要ないため、forループもwhileループも存在しません。
命令型言語にfor / whileループがある理由は、それらを変更することで状態を処理しているためです。しかし、実際には、異なる視点から見た場合、whileブロックを関数自体として考え、パラメーターを取得し、それを処理し、新しい状態を返します(異なるパラメーターで同じ関数を呼び出すこともできます)ループを再帰と考えることができます。
世界は、可変または不変として定義することもできます。世界を一連のルールとして定義し、すべてのルールと現在の状態をパラメーターとして取る究極の関数を呼び出し、同じ機能を持つこれらのパラメーターに従って新しい状態を返します(同じ状態で次の状態を生成します)方法)、それは再帰とループであると言うこともできます。
次の例では、lifeは関数が2つのパラメーター「rules」と「state」を取り、新しい状態は次回のティックで構築されます。
life rules state = life rules new_state
where new_state = construct_state_in_time rules state
[1]:末尾呼び出しの最適化は、新しい関数を作成する代わりに再帰呼び出しで既存の関数スタックを使用するための関数型プログラミング言語の一般的な最適化です。
[2]:MITのコンピュータープログラムの構造と解釈。https://mitpress.mit.edu/books/structure-and-interpretation-computer-programs
whileループは再帰とは異なります。
関数が呼び出されると、次のことが行われます。
スタックフレームがスタックに追加されます。
コードポインターが関数の先頭に移動します。
whileループが終了すると、次のことが発生します。
条件は、何か真かどうかを尋ねます。
その場合、コードはポイントにジャンプします。
一般に、whileループは次の擬似コードに似ています。
if (x)
{
Jump_to(y);
}
最も重要なのは、再帰とループのアセンブリコード表現とマシンコード表現が異なることです。これは、それらが同じではないことを意味します。同じ結果になる可能性がありますが、異なるマシンコードは、100%同じではないことを証明しています。
繰り返しだけでは再帰とほぼ同等では不十分ですが、スタックでの繰り返しはほぼ同等です。再帰関数は、スタックを使用した反復ループとして再プログラムでき、その逆も可能です。ただし、これは実用的であることを意味するものではなく、特定の状況では、いずれかの形式が他のバージョンよりも明確なメリットをもたらす場合があります。
なぜこれが議論の余地があるのかはわかりません。スタックでの再帰と反復は、同じ計算プロセスです。いわば、同じ「現象」です。
私が考えることができる唯一のことは、これらを「プログラミングツール」として見るとき、それらを同じものと考えてはならないことに同意するということです。それらは「数学的に」または「計算的に」同等です(繰り返しではなく、スタックでの繰り返し)実装/問題解決の観点から、いくつかの問題は何らかの形でうまく機能する可能性があり、プログラマーとしてのあなたの仕事はどちらがより適しているかを正しく決定することです。
明確にするために、whileループは本質的に再帰ですか?は明確なnoであるか、少なくとも「スタックがない限り」ではありません。