再帰呼び出し、よく使う?使わない? 141
ストーリー by headless
道具 部門より
道具 部門より
本家/.「AP Test's Recursion Examples: An Exercise In Awkwardness」より
FacebookのAP Computer Sicence(大学先修課程のコンピューターサイエンス科目)グループ(非公開)に投稿された、再帰呼び出しを使って0から6の数字を出力するコード例について、「APの試験で粗末なコーディング技術が使われていることを示す例がまた現れた」とAlfred Thompson氏が皮肉った。Thompson氏は「我々はしばしば、コード例に理想的ではないコーディング技術を使わざるを得ないこともある」と指摘。「通常は使うことのないコードを例にするのは、物事を明確にし、特定の概念を説明するためだ。特に再帰呼び出しを必要とする例はかなり複雑になる傾向があるので、このようなコード例は再帰呼び出しの解説で多く使われているようだ。」という。「0123456」を出力するためにループではなく再帰呼び出しを使用しているのは再帰呼び出しの処理を教えるためではあるのだが、Thompson氏はこれも粗末なコーディング技術の例であると主張する。
ただし、「再帰呼び出しで反復処理を行うのが一般的な関数型プログラミングを学んできた人は、当然これに同意しないだろう。」と付け加える。「金槌しか持っていなければ、すべての問題が釘に見えるなどと言われるが、再帰呼び出しとループの問題も同様だ。反復処理のために最初に選んだ(または最初に学んだ)のがループであれば、再帰呼び出しを解決方法だとは思わない傾向がある。同様に(関数型プログラミングで一般的な)再帰呼び出しから始めたのであれば、再帰呼び出しが反復処理のためのものだと考えるだろう。」とのことだ。皆さんの場合、再帰呼び出しをよく使う傾向があるだろうか。それとも避ける傾向があるだろうか。
どっちで考えるかだけ (スコア:2, 興味深い)
ロジックを設計した段階で再帰的になってれば再帰呼び出しで実装するし
ロジックを設計した段階で単純ループになってればループ構造で実装する。
そのように考える方が分かりやすいからそういうロジック設計になっているのだし
リソースが潤沢な時代だからどっちでもいい。
Re:どっちで考えるかだけ (スコア:2)
趣味では使うが、仕事では積極的に使わない。
ハイエンドのマシンを使う顧客とか、ピーク時でギリギリ扱えるぐらいの余力だったりする。
やはり、リソースが潤沢という前提が成り立つかどうかかが、業務などの環境依存なので環境依存のコードを入れるのは避けたいところ。
色んな顧客がいて、負荷がかかるコンポーネントも少しずつ違うし、負荷特性の違う顧客にそれぞれの顧客に対して、
再起呼び出しで使うスタックサイズが十分であることを保証し続けることが面倒。
顧客のシステムが運用中にスタックオーバフローで落ちるとか目も当てられないので、
まずは、見積もり式が必要になるが、これを作成し、メンテナンスし続けるのが手間。
再起関数を修正すると、必要なスタックサイズも変更になるし、その度に再見積もりさせる訳にもいかない。
というより、再起呼び出しを使わなくても、スタックに置くデータサイズには気を使う。
ヒープ見積もりも、見積もりしやすいようにデータ構造の管理方法とか工夫しているくらい。
Re:どっちで考えるかだけ (スコア:1)
とはいえスタックサイズを過信しないほうがよいと思う。
Re:どっちで考えるかだけ (スコア:1)
> 計算量O(N)以上のアルゴリズムの実装には再帰を使わない方がいいよ。
クイックソート(ピボットより小さい側と大きい側の二つ)とか、木のトラバース(2分木なら左の枝と右の枝)といった「自分自身を2回以上呼び出す」ような分割統治されたコードを書くときは、まず再帰を使いますね。末尾再帰とかループ化は簡単にはできません。
自前でキューとか作って管理すれば再帰を無くせますが、可読性が格段に落ちますし。そんなことをするぐらいなら、関数呼び出しの引数を最小化する(データ本体は別に持たせて、参照のインデックスだけ再帰呼び出しの引数にする)などといったスタック消費量削減を目指した方が楽かと。
#クイックソートも計算量O(N logN)で、O(N)より大きいよ。ていうか、「計算量がO(N)より小さい」ような「再帰を使う」アルゴリズムってめったにないよなぁ。
#二分探索ぐらいかですかね?。あれこそ再帰を使うのがばからしいというか、簡単に再帰無しのループに書き下せる感じだけど…
まあ、2回呼び出しがあるからといってフィボナッチを再帰で組むヤツがいたとしたら、そいつはさすがに許せないな。
再帰とループの違いが全くわからん (スコア:2)
/* どうちがうのか、自分には全くわからない。gotoは0引数の自分への再帰でしょう? */
/* 末尾最適化できないのはコンパイラかプログラマがタコなだけ。 */
#include <stdio.h>
int x=0;
int main(){
main:
x++;
printf("%d\n",x);
if(x<100){
goto main;
}else{
exit 0;
}
}
#include <stdio.h>
int x=0;
int main(){
x++;
printf("%d\n",x);
if(x<100){
main();
}else{
exit 0;
}
}
新人。プログラマレベルをポケモンで言うと、コラッタぐらい
使うこともある (スコア:1)
再帰的なデータ構造を扱うとき(ツリーをトラバースするときなど)は、再帰呼び出しを直接使うことがあります。
その他、末尾呼び出しがスタックフレームを消費しない言語を使うときは、継続条件が繰り返しの途中に現れるようなループを書く際に、再帰呼び出しを直接使います。
より抽象化された高階関数などが使える時は、当然ながらそちらを使うべきだと思います。
Re:使うこともある (スコア:1)
>再帰的なデータ構造を扱うとき(ツリーをトラバースするときなど)は、再帰呼び出しを直接使うことがあります。
これは再帰を使わないほうが難しいね
Re: (スコア:0)
> より抽象化された高階関数などが使える時は、当然ながらそちらを使うべきだと思います。
関数型言語のコミュニティでも、再帰呼び出しを直接書くのは好ましくないスタイルとされているはずです
ケースバイケースですけども、mapやfoldrが使えるのにオレ再帰は歓迎されません
Re: (スコア:0)
個人的な印象だと、動作や検証などフローに着眼するものは高階関数より再帰の書き下しのほうが理解しやすい気がします
値を生成するものは高階関数が読み書きのどちらも具合がいい
あくまで個人的な印象です
技術的な問題がある (スコア:1)
末尾呼び出しの最適化が仕様で保証された言語以外でループ代わりに使うと、あっという間にスタックを使い切る。(再帰的定義で繰り返しを表す発想の起源である)数学ではスタックの深さは無限だから問題にならないだろうけど(ただし停止することの証明が必要)。
Re: (スコア:0)
数学的帰納法は停止しないよね!…するのか?どっちだろ。
Re:技術的な問題がある (スコア:1)
たかだかω回で停止する。(数学的にはそういうのは「停止する」うちに入る)
Re: (スコア:0)
最初にプログラムを作った時は問題なくても、
繰り返し何度も修正をしたりすると(他人がコードを直すとかあったりすると)スタックオーバーフローとかよくありそうw
Re: (スコア:0)
やはりスタックがどんどん積み上がるのをイメージしてしまい躊躇しますよね。実際はほとんど実害ないのでしょうけど
ソートで教えればいいやん (スコア:1)
クイックソート等で再帰呼出使えば、両方教えられて一石二鳥だと思う
Re:ソートで教えればいいやん (スコア:2)
ポインタで折れなかったCの初学者が次に迎える難関が、再帰の例としてクイックソートを示されることですね。
Re:ソートで教えればいいやん (スコア:1)
つqsort(3)
使い方が分からなくて自前でバブルなソートを書いた奴がいた。
-------- tear straight across --------
Re:ソートで教えればいいやん (スコア:1)
多分それは関数をパラメータとして渡すということが分からなかったのではないかと.
Re:ソートで教えればいいやん (スコア:1)
マージソートにしとけ。その方が無難。
#あえて罠にはめて学習させるのが目的なら別だけど。
ソートで教えるのは最悪 (スコア:1)
クイックソートこそが、再帰呼び出しを使うのに最も不適切な例の一つじゃね。
入門書に載ってることが多いけど、マネすると酷い目に遭う。
Re:ソートで教えるのは最悪 (スコア:1)
「マネすると酷い目に遭う」ことこそが、教えておくべきことだったのだよ。
再帰呼び出しの階層の深さが予測可能なら (スコア:1)
Re:再帰呼び出しの階層の深さが予測可能なら (スコア:2)
> でも最近の言語は、イテレーターや無名関数が充実してるのであえて書くことも無い。
無名関数の中身も用意してくれるとは、最近の言語は至れり尽くせりだな
-------- tear straight across --------
自分を信じないこと (スコア:1)
Cのポインタとかもそうなんだけど、「便利だしその機能であるほうが自然で有利」っていうのはある(なので使う時は使う)
ただ、他コメントから引用すれば「プログラマがタコ」が悪をもたらすなら、できれば利用しないですむようにするのがいいよなぁ。とは思う。
# むろん言語の実装によって、スタックはそもそも使わないとか害がないタイプもあるだろうけど
# ちゃんとしないと末尾再帰が最適化されないとか、ただのトラップにしかならんときもある。
まあ、ケースバイケース、なんですが。
M-FalconSky (暑いか寒い)
わかるだろ (スコア:1)
再帰が必要なら再帰にする。
必要かどうかがわからないなら、まだケツが青いってこった。
ツリー状のデータを扱うと (スコア:1)
どうしても必要になってきますね。
階層が浅ければ階層分の関数を用意すればいいような気がしますがそれだとコピーコードになるのでだめですね
再帰呼び出しを必要とする例 (スコア:0)
public void listRecurse(File directory) {
for (File file: directory.listFiles()) {
System.out.println(file.getPath());
if (file.isDirectory()) listRecurse(file);
}
}
Re:再帰呼び出しを必要とする例 (スコア:2, 参考になる)
キューを使う方法も知っておくと、今後の人生とメモリ使用量が豊かになるでしょう。
Re:再帰呼び出しを必要とする例 (スコア:1)
データ構造が再帰的なんだから
アルゴリズムが再帰的になるのは当たり前
元の主張は,意訳すると,反復処理をわざわざ再帰で書き直すことに意味が無いと言ってる.
Re: (スコア:0)
ああ、俺もフォルダ階層潜る時にしかつかってないかも。。
日常的に使っている (スコア:0)
Javascriptでajaxだして、その結果をみてまたajaxだして、...
というプログラムは再帰にせざるを得ない。
エディタが一畳分くらいの面積で全部を見渡せるほどの注意力があれば非同期ハンドラを入れ子にすればいいのかもしれないが。
それ以外にもDOMトラバースとか、再帰でやる方がコードが見やすい。
みんなもそうだよね?
Re: (スコア:0)
つ[Promise]
Re: (スコア:0)
だから、$.ajax()のdone()の中で$.ajaxが必要なら再帰になる。と言っているわけですが。
Re:日常的に使っている (スコア:1)
そのコードは、「endlessAjax 関数内で、endlessAjax関数の実行予約(setTimeout)している」だけで、endlessAjaxの呼び出しはしていない。
実際の呼び出しフローとしては、
1) 何かをトリガーにendlessAjaxを呼び出す→すぐに実行処理終了(非同期)
2) データが届くと、システムが、イベント設定された無名関数を呼び出す→すぐに実行処理終了
3) 一分立ったら、システムが、イベント設定されたendlessAjaxを呼び出す→すぐに実行処理終了(非同期)
→2に戻る
という繰り返しで、全然再帰じゃないですよ。
イベントドリブンな環境/システムだと、イベント処理では基本的に再帰は出てこないと思う。イベントを発生させても、キューに一旦溜まるだけなのが普通。
#Win32APIのSendMessage なんかは、投げたメッセージがその場で処理されるので、再帰になりますね。無限ループにならないよう注意が必要…
中断や再開 (スコア:0)
中断や再開といった処理が必要になった場合困るので
自分的には避けられるならば避ける。
具体的には例えばGUIのある場合、
再起ループだとマウスへの応答に支障が出る。
その場合、自前でスタック構造を組んでループ。
場合によってはさらにインタープリターを組んでVMを構成する。
Re:中断や再開 (スコア:1)
別スレッドでやれ。(本来の意味で)
使うかな (スコア:0)
スタックが十分にあれば
読みやすくなるかどうか、が第一でしょう (スコア:0)
再帰で書いたほうが読みやすくなるなら使うし、そうでないなら使いませんよね。
ただし、後世メンテナンスする人間の質が期待できない場合は
再帰を使ったほうがすっきりするケースでもあえてループで書く場合もあります。
再帰呼び出しで反復処理を行うのが一般的? (スコア:0)
プログラミング言語としては関数型であろうとも、ループの機能を持っていないものは無いのでは?
関数型プログラミングの原理/スタイルを学ぶ時には再帰で反復処理を記述したとしても、現実のアプリケーション開発ではループを使わないはずが無いでしょう
(そもそも再帰的なアルゴリズムを実装したり、再帰の方が簡明に記述できる処理をするのなら話は別だが)
Re:再帰呼び出しで反復処理を行うのが一般的? (スコア:2)
再帰は深さが不定なツリーを舐めるような処理、つまり文字通り「再帰的」な処理に適してる。
ループじゃキツイでしょう?
Re:再帰呼び出しで反復処理を行うのが一般的? (スコア:1)
SQL
未だにループは無い
再帰共通表式ができるまで、チューリング完全ではなかったぐらいだ
Re:再帰呼び出しで反復処理を行うのが一般的? (スコア:2)
> チューリング完全ではなかったぐらいだ
チューリング完全でなかったのは意図的な仕様でしょ。
プログラミング言語がチューリング完全であるというのは
必ずしも望ましい特性ではない。
Re:再帰呼び出しで反復処理を行うのが一般的? (スコア:1)
Re: (スコア:0)
HaskellにはForもWhileもありません。
機械的にコーディングすればいいだけの会社なら命令型以外のパラダイムなんて知る必要もないのでしょうが。
Re: (スコア:0)
whileはあるぞ…
できるだけ避けるが (スコア:0)
できるだけ避ける。あまりスタックを無駄遣いしたくない。
しかし、goto文などと同様に適切と思うときは使う。まあ、当然のことね。
好きか嫌いかと言われましても。 (スコア:0)
A → 関連データがない
├B → 関連データがある
│└C → 関連データがある
└D → 関連データがない
└E→ 関連データがある
こんな時に、 BとEを演算して、Aの結果のように振る舞うような処理とかあるし……。
必要な時に使うってだけじゃないかな。合理的な理由があるかどうかなだけで。好きも嫌いもない。
あ。でも再起の中でアレコレごちゃごちゃ処理入れてくるのは嫌い。
可能な限り探査だけで済ませたい。
Re:論外、使用禁止 (スコア:1)
ありがとうございます。
お言葉とおり甘えさせていただきます。
#だってアマチュア
Re:論外、使用禁止 (スコア:1)
便乗ですが、ユーザモードでオーバーフローしたらセグメンテーション違反で落ちるかと思うんですが、
カーネルモードでオーバーフローしたら、どうなるんでしょうか…
#カーネルモードでメモリ空間の割り当て方がよく分からないのでAC
Re:コールスタックのハード実装 (スコア:1)
____________
ヾミ || || || || || || || ,l,,l,,l 川〃彡|
V~~''-山┴''''""~ ヾニニ彡| できる・・・・・・!
/ 二ー―''二 ヾニニ┤ できるが・・・
'-.,  ̄ ̄ _,,,..-‐、 〉ニニ| 今回 まだ その方法と可読性の
/"''-ニ,‐l l`__ニ-‐'''""` /ニ二| 指定まではしていない
| ===、! `=====、 l =lべ=|
. | `ー゚‐'/ `ー‐゚―' l.=lへ|~| そのことを
|`ー‐/ `ー―― H,〉|=| どうか諸君らも
| / 、 l|__ノー| 思い出していただきたい
. | /`ー ~ ′ \ .|ヾ.ニ|ヽ
|l 下王l王l王l王lヲ| | ヾ_,| \ つまり・・・・
. | ≡ | `l \__ 我々がその気になれば
!、 _,,..-'′ /l | ~''' パラメータの受け渡しは明示的に
‐''" ̄| `iー-..,,,_,,,,,....-‐'''" / | | スタック キューということも
-―| |\ / | | 可能だろう・・・・・・・・・・ということ・・・・!