web-dev-qa-db-fra.com

Quelle est la différence entre récursivité et corécursion?

Quelle est la différence entre ceux-ci?

Sur Wikipédia, il y a peu d'informations et aucun code clair expliquant ces termes.

Quels sont des exemples très simples expliquant ces termes?

Comment la corécursion est-elle le double de la récursivité?

Existe-t-il des algorithmes corécusifs classiques?

58
user167908

Il y a plusieurs bonnes façons de voir les choses. La chose la plus simple pour moi est de penser à la relation entre "définitions inductives" et "définitions coinductives"

Une définition inductive d'un ensemble va comme ceci.

L'ensemble "Nat" est défini comme le plus petit ensemble tel que "Zero" est dans Nat, et si n est dans Nat "Succ n" est dans Nat.

Ce qui correspond à l'Ocaml suivant

type nat = Zero | Succ of nat

Une chose à noter à propos de cette définition est qu’un certain

omega = Succ(omega)

n'est PAS membre de cet ensemble. Pourquoi? Supposons que c'était le cas, considérons maintenant l'ensemble N qui a tous les mêmes éléments que Nat, sauf qu'il n'a pas d'oméga. Clairement, zéro est dans N, et si y est dans N, Succ (y) est dans N, mais N est plus petit que Nat, ce qui est une contradiction. Donc, l'oméga n'est pas dans Nat.

Ou, peut-être plus utile pour un informaticien:

Étant donné un ensemble "a", l'ensemble "List of a" est défini comme le plus petit ensemble tel que "Nil" est dans la liste de a, et que si xs est dans la liste de a et x est dans un "Cons x xs" est dans la liste de a.

Ce qui correspond à quelque chose comme

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

Le mot clé ici est "le plus petit". Si nous ne disions pas "le plus petit", nous n'aurions aucun moyen de savoir si l'ensemble Nat contenait une banane!

Encore,

zeros = Cons(Zero,zeros)

n'est pas une définition valide pour une liste de nats, tout comme l'oméga n'était pas un Nat valide.

Définir données de manière inductive comme ceci nous permet de définir les fonctions qui y travaillent en utilisant récursivité

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

nous pouvons alors prouver des faits à ce sujet, comme "plus un zéro = a" en utilisant induction (en particulier, l'induction structurelle)

Notre preuve procède par induction structurelle sur a.
Pour le cas de base, a soit zéro. plus Zero Zero = match Zero with |Zero -> Zero | Succ(c) -> let r = plus c b in Succ(r) donc nous connaissons plus Zero Zero = Zero. Soit a un nat. Supposons l'hypothèse inductive que plus a Zero = a. Nous montrons maintenant que plus (Succ(a)) Zero = Succ(a) c'est évident puisque 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) Ainsi, par induction plus a Zero = a Pour tous a en nat

Nous pouvons bien sûr prouver des choses plus intéressantes, mais c'est l'idée générale.

Jusqu'à présent, nous avons traité des données définis par induction que nous avons obtenus en le laissant être le "plus petit" ensemble. Alors maintenant, nous voulons travailler avec coinductivement défini codata que nous obtenons en le laissant être le plus grand ensemble.

Donc

Soit a un ensemble. L'ensemble "Stream of a" est défini comme le plus grand ensemble tel que pour chaque x dans le flux de a, x se compose de la paire ordonnée (tête, queue) telle que la tête est dans un et la queue est dans le flux de

À Haskell, nous exprimerions ceci comme

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

En fait, dans Haskell, nous utilisons normalement les listes intégrées, qui peuvent être une paire ordonnée ou une liste vide.

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

La banane n'est pas non plus un membre de ce type, car ce n'est pas une paire ordonnée ou la liste vide. Mais maintenant, nous pouvons dire

ones = 1:ones

et c'est une définition parfaitement valable. De plus, nous pouvons effectuer une co-récursivité sur ces co-données. En fait, il est possible qu'une fonction soit à la fois co-récursive et récursive. Alors que la récursivité a été définie par la fonction ayant un domaine composé de données, la co-récursivité signifie simplement qu'elle a un co-domaine (également appelé la plage) qui est co-données . La récursivité primitive signifiait toujours "s'appeler" sur des données plus petites jusqu'à atteindre les plus petites données. La co-récursivité primitive "s'appelle" toujours sur des données supérieures ou égales à ce que vous aviez auparavant.

ones = 1:ones

est primitivement co-récursif. Alors que la fonction map (un peu comme "foreach" dans les langages impératifs) est à la fois primitivement récursive (en quelque sorte) et primitivement co-récursive.

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

il en va de même pour la fonction zipWith qui prend une fonction et une paire de listes et les combine en utilisant cette fonction.

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

l'exemple classique des langages fonctionnels est la séquence de Fibonacci

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

qui est primitivement récursif, mais peut être exprimé plus élégamment comme une liste infinie

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

un exemple intéressant d'induction/coinduction prouve que ces deux définitions calculent la même chose. Ceci est laissé comme un exercice pour le lecteur.

26
Philip JF

Fondamentalement, la corécursion est une récursion de type accumulateur , construisant son résultat sur la marche à suivre à partir du cas de départ, tandis que la récursivité régulière construit son résultat sur le chemin du retour à partir de le cas de base.

(parlant Haskell maintenant). C'est pourquoi foldr (avec une fonction de combinaison stricte) exprime la récursivité, et foldl' (avec un peigne strict f.)/scanl/until/iterate/unfoldr/etc. express corecursion. La corecursion est partout. foldr avec un peigne non strict. F. exprime contre récursion modulo de la queue .

Et la récursion gardée de Haskell est juste comme contre la récursion modulo de la queue .

C'est la récursivité:

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]

(lis $ comme "de"). C'est la 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
      = .....

Plis: http://en.wikipedia.org/wiki/Fold_ (fonction d'ordre supérieur)

11
Will Ness

Vérifiez cela sur le blog de Vitomir Kovanovic . Je l'ai trouvé au point:

Évaluation paresseuse dans une fonctionnalité très agréable trouvée dans les langages de programmation avec des capacités de programmation fonctionnelles telles que LISP, haskell, python etc. Cela signifie que l'évaluation de la valeur de la variable est retardée à l'utilisation réelle de cette variable.

Cela signifie que, par exemple, si vous vouliez créer une liste de millions d'éléments avec quelque chose comme ça (defn x (range 1000000)) elle n'est pas réellement créée, mais elle est simplement spécifiée et lorsque vous utilisez vraiment cette variable pour la première fois, par exemple lorsque vous voulez que le 10e élément de cette liste, l'interpréteur crée uniquement les 10 premiers éléments de cette liste. Ainsi, la première exécution de (prendre 10 x) crée réellement ces éléments et tous les appels ultérieurs à la même fonction fonctionnent avec des éléments déjà existants.

Ceci est très utile car vous pouvez créer des listes infinies sans erreurs de mémoire insuffisante. La liste sera grande juste combien vous avez demandé. Bien sûr, si votre programme fonctionne avec de grandes collections de données, il peut atteindre une limite de mémoire dans l'utilisation de ces listes infinies.

D'autre part corecursion est double à récursivité. Qu'est-ce que cela signifie? Eh bien, tout comme les fonctions récursives, qui sont exprimées en termes d'elles-mêmes, les variables corécursives sont exprimées en termes d'elles-mêmes.

Ceci est mieux exprimé sur l'exemple.

Disons que nous voulons une liste de tous les nombres premiers ...

4
Priyadarshi Kunal