TL; DR :再帰は、遍在する帰納的に定義されたデータを処理するために使用されます。
より高いレベルの抽象化で操作する場合、再帰は自然です。関数型プログラミングは、関数を使ったコーディングだけではありません。それは、自然に関数を使用する、より高いレベルの抽象化を操作することです。関数を使用する場合、意味のあるコンテキストから、同じ関数を再利用する(再度呼び出す)のは自然なことです。
世界は、類似/同じビルディングブロックの繰り返しによって構築されます。生地を2つに切ると、生地が2つになります。数学的帰納法は数学の中心です。私たち人間は数えます(1,2,3 ...のように)。帰納的に定義されたもの(たとえば、{1からの数}は{1であり、2からの数})は、それが定義/構築されたのと同じケースに従って、再帰関数によって処理/分析するのが自然です。
再帰はどこにでもあります。反復ループはとにかく変装した再帰です。なぜなら、そのループに再び入ると、同じループに再び入るからです(おそらく異なるループ変数を使用して)。つまり、コンピューティングに関する新しい概念を発明するのではなく、基盤を発見してそれを明示的にするようなものです。
したがって、再帰は自然です。問題に関するいくつかの法則、(関数がコヒーレントに定義されているという仮定の下で)不変条件を保持する定義中の関数を含む方程式、簡略化された用語で問題を再指定する、そして出来上がり!解決策があります。
例、リストの長さを計算する関数(帰納的に定義された再帰データ型)。それが定義されていると仮定し、当然のことながら、リストの長さを返します。それが従わなければならない法律は何ですか?問題をどのように単純化しても、どの不変条件が保持されますか?
最も直接的なのは、リストをそのヘッド要素に分解し、残りの部分(別名、リストのテール)を分解することです(リストの定義/構築方法による)。法律は、
length (x:xs) = 1 + length xs
えっ!しかし、空のリストはどうですか?それはそれでなければなりません
length [] = 0
では、どのようにそのような関数を書くのでしょうか?...待ってください...私たちはすでにそれを書いています!(Haskellでは、関数適用が並置で表現されているのか疑問に思っている場合、括弧はグループ化のためだけに使用され、最初の要素と残りの要素(x:xs)
を含むリストです)。x
xs
このようなスタイルのプログラミングを可能にするために必要な言語は、TCO(そしておそらく少し贅沢な TRMCO)を備えていることだけなので、スタックの爆発はなく、準備は整っています。
もう1つは、純度です。コード変数やデータ構造(レコードのフィールドなど)の不変性です。
これにより、何がいつ変化するかを追跡する必要がなくなるだけでなく、「変化する」可変変数/データに隠れるのではなく、コードで時間を明示的に明らかにすることができます。これからは、命令型コードで変数の値を「変更」することしかできません。過去にその値をうまく変更することはできませんね。
そのため、記録された変更履歴のリストが作成され、変更はコードで明示的に示されます。代わりに、x := x + 2
を記述しlet x2 = x1 + 2
ます。それはコードについての推論をとても簡単にします。
TCOを使用した末尾再帰のコンテキストでの不変性に対処するには、length
アキュムレータ引数パラダイムの下での上記の関数のこの末尾再帰の書き換えを検討してください。
length xs = length2 0 xs -- the invariant:
length2 a [] = a -- 1st arg plus
length2 a (x:xs) = length2 (a+1) xs -- the length of 2nd arg
ここで、TCOは、直接ジャンプに加えて、呼び出しフレームの再利用を意味します。したがって、の呼び出しチェーンはlength [1,2,3]
、関数のパラメーターに対応する呼び出しスタックフレームのエントリを実際に変更していると見なすことができます。
length [1,2,3]
length2 0 [1,2,3] -- a=0 (x:xs)=[1,2,3]
length2 1 [2,3] -- a=1 (x:xs)=[2,3]
length2 2 [3] -- a=2 (x:xs)=[3]
length2 3 [] -- a=3 (x:xs)=[]
3
値を変更するプリミティブがない純粋な言語では、変更を表現する唯一の方法は、更新された値を引数として関数に渡し、さらに処理することです。それ以降の処理が以前と同じである場合、当然のことながら、同じ関数を呼び出して、更新された値を引数として渡す必要があります。そして、それは再帰です。
そして、以下は、引数リストの長さを計算する履歴全体を明示的にします(そして必要に応じて再利用できるようにします):
length xs = last results
where
results = length3 0 xs
length3 a [] = [a]
length3 a (x:xs) = a : length3 (a+1) xs
Haskellでは、これはガード付き再帰またはコアカーションとしてさまざまに知られています(少なくとも私はそう思います)。