web-dev-qa-db-ja.com

再帰とコアカージョンの違いは何ですか?

これらの違いは何ですか?

ウィキペディアには情報がほとんどなく、これらの用語を説明する明確なコードはありません。

これらの用語を説明する非常に単純な例は何ですか?

コアカージョンはどのように再帰のデュアルですか?

古典的な共焦点アルゴリズムはありますか?

58
user167908

これを見るにはいくつかの良い方法があります。私にとって最も簡単なのは、「帰納的」と「共同帰納的定義」の関係について考えることです

セットの帰納的定義はこのようになります。

セット「Nat」は、「ゼロ」がNatにあり、nがNatにある場合、「Succ n」がNatにあるような最小のセットとして定義されます。

これは次のOcamlに対応します

_type nat = Zero | Succ of nat
_

この定義について注意すべきことの1つは、

_omega = Succ(omega)
_

はこのセットのメンバーではありません。どうして?それがそうであったと仮定して、今度はオメガを持たないことを除いてナットとすべて同じ要素を持つセットNを考えてください。明らかにゼロはNにあり、yがNにある場合、Succ(y)はNにありますが、Nはナットよりも小さいため、矛盾です。だから、オメガはナットにありません。

または、おそらくコンピュータ科学者にとってより有用です:

セット「a」を考えると、セット「List of a」は、「Nil」がaのリストにあり、xsがaのリストにある場合、最小セットとして定義されます。 xは「短所x xs」はaのリストにあります。

これは次のようなものに対応します

_type 'a list = Nil | Cons of 'a * 'a list
_

ここでの術語は「最小」です。 「最小」と言わなかった場合、Natがバナナを含んでいるかどうかを知る方法はありません。

再び、

_zeros = Cons(Zero,zeros)
_

は、オメガが有効なナットではなかったのと同じように、ナットのリストの有効な定義ではありません。

このように帰納的にdataを定義すると、recursionを使用して機能する関数を定義できます

_let rec plus a b = match a with
                   | Zero    -> b
                   | Succ(c) -> let r = plus c b in Succ(r)
_

次に、これについての事実を証明できます。たとえば、「プラスaゼロ= a」のように、誘導(具体的には、構造誘導)を使用します。

私たちの証明はaの構造帰納によって進行します。
ベースケースの場合は、ゼロになります。 plus Zero Zero = match Zero with |Zero -> Zero | Succ(c) -> let r = plus c b in Succ(r)は_plus Zero Zero = Zero_です。 aをnatにします。 _plus a Zero = a_という帰納的仮説を仮定します。これで、plus (Succ(a)) Zero = Succ(a)plus (Succ(a)) Zero = match a with |Zero -> Zero | Succ(a) -> let r = plus a Zero in Succ(r) = let r = a in Succ(r) = Succ(a)から明らかであることを示します。したがって、誘導によって_plus a Zero = a_すべてのaのnat

もちろん、もっと興味深いことが証明できますが、これは一般的な考え方です。

これまでは、帰納的に定義されたdataを扱いましたが、これを「最小の」セットとして取得しました。だから今、私たちはそれを最大のセットとすることで得られる、同時伝導的に定義されたcodataで作業したいと思います。

そう

セットにしましょう。セット「Stream of a」は、aのストリーム内の各xに対して、xが順序付きペア(head、tail)で構成され、headがaにあり、tailがa

Haskellではこれを次のように表現します

_data Stream a = Stream a (Stream a) --"data" not "newtype"
_

実際、Haskellでは通常、組み込みのリストを使用します。これは、順序付けられたペアまたは空のリストにすることができます。

_data [a] = [] | a:[a]
_

バナナは順序付けられたペアでも空のリストでもないため、このタイプのメンバーでもありません。しかし、今、私たちは言うことができます

_ones = 1:ones
_

これは完全に有効な定義です。さらに、この共同データに対して再帰を実行できます。実際には、関数が再帰的でも再帰的でもあり得ます。再帰は、データで構成されるドメインを持つ関数によって定義されましたが、共再帰は、co-dataであるco-domain(範囲とも呼ばれる)があることを意味します。プリミティブな再帰とは、いくつかの最小データに到達するまで、常に小さいデータに対して「自分自身を呼び出す」ことを意味しました。プリミティブな再帰は、以前のデータと同じかそれ以上のデータを常に「呼び出し」ます。

_ones = 1:ones
_

原始的に再帰的です。関数map(命令型言語の "foreach"のようなもの)は、基本的に再帰的(一種)であり、基本的に再帰的です。

_map :: (a -> b) -> [a] -> [b]
map f []     = []
map f (x:xs) = (f x):map f xs
_

関数zipWithについても同様です。関数とリストのペアを取り、それらをその関数を使用して結合します。

_zipWith :: (a -> b -> c) -> [a] -> [b] -> [c]
zipWith f (a:as) (b:bs) = (f a b):zipWith f as bs
zipWith _ _ _           = [] --base case
_

関数型言語の古典的な例はフィボナッチ数列です

_fib 0 = 0
fib 1 = 1
fib n = (fib (n-1)) + (fib (n-2))
_

これは基本的に再帰的ですが、無限リストとしてよりエレガントに表現できます

_fibs = 0:1:zipWith (+) fibs (tail fibs)
fib' n = fibs !! n --the !! is haskell syntax for index at
_

帰納法/コインダクション法の興味深い例は、これら2つの定義が同じことを計算することを証明しています。これは読者のための演習として残されています。

26
Philip JF

基本的に、corecursionは再帰accumulator-styleであり、最初のケースから順方向に結果を構築しますが、通常の再帰は、逆方向から結果を構築しますベースケース。

(今Haskellといえば)。そのため、foldr(厳密な結合関数を使用)は再帰を表し、foldl'(厳密なコームを使用。f。)/ scanl/until/iterate/unfoldr /などは、コアカーションを表します。 Corecursionはどこにでもあります。 foldr非厳密な櫛。 f。 tail recursion modulo cons を表します。

そして、Haskellの保護された再帰 is justtail recursion modulo cons のように。

これは再帰です:

fib n | n==0 = 0
      | n==1 = 1
      | n>1  = fib (n-1) + fib (n-2)

fib n = snd $ g n
  where
    g n | n==0 = (1,0)
        | n>0  = let { (b,a) = g (n-1) } in (b+a,b)

fib n = snd $ foldr (\_ (b,a) -> (b+a,b)) (1,0) [n,n-1..1]

(読んだ $ as "of")。これはcorecursionです:

fib n = g (0,1) 0 n where
  g n (a,b) i | i==n      = a 
              | otherwise = g n (b,a+b) (i+1)

fib n = fst.snd $ until ((==n).fst) (\(i,(a,b)) -> (i+1,(b,a+b))) (0,(0,1))
      = fst $ foldl (\(a,b) _ -> (b,a+b)) (0,1) [1..n]
      = fst $ last $ scanl (\(a,b) _ -> (b,a+b)) (0,1) [1..n]
      = fst (fibs!!n)  where  fibs = scanl (\(a,b) _ -> (b,a+b)) (0,1) [1..]
      = fst (fibs!!n)  where  fibs = iterate (\(a,b) -> (b,a+b)) (0,1)
      = (fibs!!n)  where  fibs = unfoldr (\(a,b) -> Just (a, (b,a+b))) (0,1)
      = (fibs!!n)  where  fibs = 0:1:map (\(a,b)->a+b) (Zip fibs $ tail fibs)
      = (fibs!!n)  where  fibs = 0:1:zipWith (+) fibs (tail fibs)
      = (fibs!!n)  where  fibs = 0:scanl (+) 1 fibs
      = .....

フォールド: http://en.wikipedia.org/wiki/Fold_(higher-order_function)

11
Will Ness

これを Vitomir Kovanovicのブログ で確認してください。私はそれを要点まで見つけました:

LISP、haskell、pythonなど)などの関数型プログラミング機能を備えたプログラミング言語に見られる非常に優れた機能の遅延評価。変数の値の評価が、その変数の実際の使用に遅れることになります。

これは、たとえば、次のような(defn x (range 1000000))のような100万個の要素のリストを作成したい場合、実際には作成されませんが、指定されただけで、初めてその変数を初めて使用するとき、たとえばそのリストの10番目の要素が必要な場合、インタープリターはそのリストの最初の10個の要素のみを作成します。したがって、最初の実行(take 10 x)は実際にこれらの要素を作成し、同じ関数への後続のすべての呼び出しは既存の要素で機能します。

メモリ不足エラーなしで無限リストを作成できるので、これは非常に便利です。リストは、要求した量だけ大きくなります。もちろん、プログラムが大量のデータコレクションを処理している場合、これらの無限リストの使用でメモリ制限に達する可能性があります。

一方、 corecursion は再帰に対して二重です。これはどういう意味ですか?自分自身で表現される再帰関数と同様に、共起変数は自分自身で表現されます。

これは例で最もよく表されます。

すべての素数のリストが必要だとしましょう...

4