Existem várias maneiras boas de ver isso. O mais fácil para mim é pensar na relação entre "definições indutivas" e "definições coindutivas"
Uma definição indutiva de um conjunto é assim.
O conjunto "Nat" é definido como o menor conjunto, de modo que "Zero" esteja em Nat e, se n estiver em Nat, "Succ n" estará em Nat.
Que corresponde ao seguinte Ocaml
type nat = Zero | Succ of nat
Uma coisa a notar sobre essa definição é que um número
omega = Succ(omega)
NÃO é um membro deste conjunto. Por quê? Suponha que sim, agora considere o conjunto N que possui todos os mesmos elementos que Nat, exceto que ele não possui ômega. Claramente, Zero está em N e, se y está em N, Succ (y) está em N, mas N é menor que Nat, o que é uma contradição. Então, o ômega não está no Nat.
Ou talvez mais útil para um cientista da computação:
Dado um conjunto "a", o conjunto "Lista de a" é definido como o menor conjunto, de forma que "Nil" esteja na Lista de a, e que se xs estiver na Lista de aex esteja em "Contras x xs" está na lista de a.
O que corresponde a algo como
type 'a list = Nil | Cons of 'a * 'a list
A palavra operativa aqui é "menor". Se não disséssemos "menor", não teríamos como saber se o conjunto Nat continha uma banana!
Novamente,
zeros = Cons(Zero,zeros)
não é uma definição válida para uma lista de nats, assim como ômega não era um Nat válido.
Definir dados indutivamente como este nos permite definir funções que funcionam com recursão
let rec plus a b = match a with
| Zero -> b
| Succ(c) -> let r = plus c b in Succ(r)
podemos provar fatos sobre isso, como "mais um zero = a" usando indução (especificamente, indução estrutural)
Nossa prova procede por indução estrutural em a.
Para o caso base, seja zero. plus Zero Zero = match Zero with |Zero -> Zero | Succ(c) -> let r = plus c b in Succ(r)
então nós sabemos plus Zero Zero = Zero
. Seja a
um nat. Assuma a hipótese indutiva de que plus a Zero = a
. Vamos agora mostrar que plus (Succ(a)) Zero = Succ(a)
isso é óbvio desde 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)
Assim, por indução plus a Zero = a
para todos a
em nat
É claro que podemos provar coisas mais interessantes, mas essa é a ideia geral.
Até agora, lidamos com dados definidos indutivamente , obtendo-os por ser o conjunto "menor". Então, agora queremos trabalhar com coinductivly definidos CODATA qual obtemos por deixá-lo ser o maior conjunto.
assim
Seja um conjunto. O conjunto "Fluxo de a" é definido como o maior conjunto, de modo que, para cada x no fluxo de a, x consiste no par ordenado (cabeça, cauda), de modo que a cabeça está em a e a cauda está no fluxo de uma
Em Haskell, expressaríamos isso como
data Stream a = Stream a (Stream a) --"data" not "newtype"
Na verdade, em Haskell, usamos normalmente as listas internas, que podem ser um par ordenado ou uma lista vazia.
data [a] = [] | a:[a]
Banana também não é um membro desse tipo, pois não é um par ordenado ou a lista vazia. Mas agora podemos dizer
ones = 1:ones
e esta é uma definição perfeitamente válida. Além disso, podemos realizar co-recursão nesses co-dados. Na verdade, é possível que uma função seja co-recursiva e recursiva. Embora a recursão tenha sido definida pela função que possui um domínio que consiste em dados, a co-recursão significa apenas que ele possui um co-domínio (também chamado de intervalo) que é co-dados. Recursão primitiva significava sempre "chamar a si mesmo" em dados menores até alcançar alguns dados menores. A co-recursão primitiva sempre "se chama" em dados maiores ou iguais aos que você possuía antes.
ones = 1:ones
é primitivamente co-recursivo. Enquanto a função map
(como "foreach" em linguagens imperativas) é primitivamente recursiva (mais ou menos) e primitivamente co-recursiva.
map :: (a -> b) -> [a] -> [b]
map f [] = []
map f (x:xs) = (f x):map f xs
O mesmo vale para a função zipWith
que pega uma função e um par de listas e os combina usando essa função.
zipWith :: (a -> b -> c) -> [a] -> [b] -> [c]
zipWith f (a:as) (b:bs) = (f a b):zipWith f as bs
zipWith _ _ _ = [] --base case
o exemplo clássico de linguagens funcionais é a sequência de Fibonacci
fib 0 = 0
fib 1 = 1
fib n = (fib (n-1)) + (fib (n-2))
que é primitivamente recursivo, mas pode ser expresso de maneira mais elegante como uma lista infinita
fibs = 0:1:zipWith (+) fibs (tail fibs)
fib' n = fibs !! n --the !! is haskell syntax for index at
Um exemplo interessante de indução / coindução está provando que essas duas definições calculam a mesma coisa. Isso é deixado como um exercício para o leitor.