it-swarm.com.de

Was ist der Unterschied zwischen Rekursion und Corecursion?

Was ist der Unterschied zwischen diesen?

Auf Wikipedia gibt es nur wenige Informationen und keinen eindeutigen Code, der diese Begriffe erklärt.

Was sind einige sehr einfache Beispiele, die diese Begriffe erklären?

Wie ist Corecursion das Duale von Rekursion?

Gibt es klassische Corecusive-Algorithmen?

58
user167908

Es gibt eine Reihe guter Sichtweisen. Für mich ist es am einfachsten, über die Beziehung zwischen "induktiven" und "koinduktiven Definitionen" nachzudenken.

Eine induktive Definition einer Menge sieht so aus.

Die Menge "Nat" ist definiert als die kleinste Menge, so dass "Null" in Nat ist, und wenn n in Nat ist, ist "Succ n" in Nat.

Welches entspricht dem folgenden Ocaml

type nat = Zero | Succ of nat

Eine Sache, die bei dieser Definition zu beachten ist, ist die Zahl

omega = Succ(omega)

ist KEIN Mitglied dieses Sets. Warum? Angenommen, es war, betrachten Sie nun die Menge N, die alle die gleichen Elemente wie Nat enthält, außer dass sie kein Omega enthält. Es ist klar, dass Null in N ist, und wenn y in N ist, ist Succ (y) in N, aber N ist kleiner als Nat, was ein Widerspruch ist. Omega ist also nicht in Nat.

Oder vielleicht nützlicher für einen Informatiker:

Bei einer Menge "a" wird die Menge "Liste von a" als die kleinste Menge definiert, so dass "Nil" in der Liste von a steht und wenn xs in der Liste von a und x in a " Nachteile x xs "ist in der Liste von a.

Welches entspricht so etwas wie

type 'a list = Nil | Cons of 'a * 'a list

Das operative Wort hier ist "kleinstes". Wenn wir nicht "kleinste" sagen würden, hätten wir keine Möglichkeit zu sagen, ob das Set Nat eine Banane enthält!

Nochmal,

zeros = Cons(Zero,zeros)

ist keine gültige Definition für eine Liste von Nats, genau wie Omega keine gültige Nat war.

Wenn wir Daten induktiv so definieren, können wir Funktionen definieren, die mit Rekursion daran arbeiten

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

wir können dann Fakten darüber beweisen, wie "plus eine Null = a" mit Induktion (speziell strukturelle Induktion)

Unser Beweis erfolgt durch strukturelle Induktion auf a.
Für den Basisfall sei a Null. plus Zero Zero = match Zero with |Zero -> Zero | Succ(c) -> let r = plus c b in Succ(r) damit wir plus Zero Zero = Zero kennen. Sei a ein Nat. Nehmen Sie die induktive Hypothese an, dass plus a Zero = a. Wir zeigen nun, dass plus (Succ(a)) Zero = Succ(a) dies offensichtlich ist, da 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) also durch Induktion plus a Zero = a Für alle a in nat

Wir können natürlich interessantere Dinge beweisen, aber das ist die allgemeine Idee.

Bisher haben wir uns mit induktiv definierten Daten befasst, die wir erhalten haben, indem wir es die "kleinste" Menge sein ließen. Jetzt wollen wir also mit koinduktiv definiertem codata arbeiten, das wir erhalten, indem wir es die größte Menge sein lassen.

Damit

Sei a eine Menge. Die Menge "Stream of a" ist definiert als die größte Menge, so dass für jedes x im Stream von a x aus dem geordneten Paar (Kopf, Schwanz) besteht, so dass sich Kopf in a und Schwanz in Stream von a befindet

In Haskell würden wir dies als ausdrücken

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

Tatsächlich verwenden wir in Haskell normalerweise die eingebauten Listen, bei denen es sich um ein geordnetes Paar oder eine leere Liste handeln kann.

data [a] = [] | a:[a]

Banana ist auch kein Mitglied dieses Typs, da es sich nicht um ein geordnetes Paar oder die leere Liste handelt. Aber jetzt können wir sagen

ones = 1:ones

und dies ist eine vollkommen gültige Definition. Darüber hinaus können wir für diese Co-Daten eine Co-Rekursion durchführen. Tatsächlich ist es möglich, dass eine Funktion sowohl rekursiv als auch rekursiv ist. Während die Rekursion durch die Funktion mit einer Domäne definiert wurde, die aus Daten besteht, bedeutet die Co-Rekursion nur, dass sie eine Co-Domäne (auch als Bereich bezeichnet) hat, die Co-Daten sind . Primitive Rekursion bedeutete, sich immer auf kleiner Daten zu "rufen", bis einige kleinste Daten erreicht waren. Die primitive Co-Rekursion "ruft" sich immer bei Daten auf, die größer oder gleich den vorherigen Daten sind.

ones = 1:ones

ist primitiv co-rekursiv. Während die Funktion map (ähnlich wie "foreach" in imperativen Sprachen) sowohl primitiv rekursiv (Art) als auch primitiv co-rekursiv ist.

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

gleiches gilt für die Funktion zipWith, die eine Funktion und ein Listenpaar aufnimmt und diese mit dieser Funktion kombiniert.

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

das klassische Beispiel für funktionale Sprachen ist die Fibonacci-Sequenz

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

das ist primitiv rekursiv, kann aber eleganter als unendliche Liste ausgedrückt werden

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

ein interessantes Beispiel für Induktion/Koinduktion ist der Beweis, dass diese beiden Definitionen dasselbe berechnen. Dies bleibt als Übung für den Leser.

26
Philip JF

Grundsätzlich ist Corecursion eine Rekursion im Akkumulatorstil , die ihr Ergebnis auf dem Weg vom Startfall nach vorne aufbaut, während die reguläre Rekursion ihr Ergebnis auf dem Rückweg von aufbaut der Basisfall.

(spricht jetzt Haskell) Deshalb drückt foldr (mit einer strengen Kombinationsfunktion) die Rekursion aus und foldl' (mit striktem Kamm f.)/scanl/until/iterate/unfoldr/etc. drücken die Kernflucht aus. Corecursion ist überall. foldr mit nicht striktem Kamm. f. drückt Schwanzrekursion modulo cons aus.

Und Haskells geschützte Rekursion ist nur wie Schwanzrekursion modulo cons .

Dies ist Rekursion:

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]

(lesen $ als "von"). Das ist Kernkurs:

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
      = .....

Falten: http: //en.wikipedia.org/wiki/Fold_ (Funktion höherer Ordnung)

11
Will Ness

Überprüfen Sie dies auf Vitomir Kovanovics Blog . Ich fand es auf den Punkt:

Faule Auswertung in einer sehr netten Funktion, die in Programmiersprachen mit funktionalen Programmierfunktionen wie LISP, haskell, python usw.) zu finden ist. Dies bedeutet, dass die Auswertung des Variablenwerts auf die tatsächliche Verwendung dieser Variablen verzögert wird.

Dies bedeutet beispielsweise, dass Sie eine Liste mit Millionen Elementen mit so etwas wie diesem erstellen möchten (defn x (range 1000000)) Es wird nicht tatsächlich erstellt, sondern nur angegeben. Wenn Sie diese Variable zum ersten Mal wirklich verwenden, z. B. wenn Sie möchten, dass das 10. Element dieses Listeninterpreters nur die ersten 10 Elemente dieser Liste erstellt. Der erste Durchlauf von (take 10 x) erstellt diese Elemente also tatsächlich und alle nachfolgenden Aufrufe derselben Funktion funktionieren mit bereits vorhandenen Elementen.

Dies ist sehr nützlich, da Sie unendliche Listen ohne Speicherfehler erstellen können. Die Liste ist genau so groß, wie Sie es angefordert haben. Wenn Ihr Programm mit großen Datenmengen arbeitet, kann es natürlich bei der Verwendung dieser unendlichen Listen an Speichergrenzen stoßen.

Andererseits ist Corecursion dual zur Rekursion. Was bedeutet das? Genau wie die rekursiven Funktionen, die in ihren Begriffen ausgedrückt werden, werden auch die kursiven Variablen in ihren Begriffen ausgedrückt.

Dies lässt sich am besten am Beispiel ausdrücken.

Nehmen wir an, wir wollen eine Liste aller Primzahlen ...

4