Ci sono molti buoni modi per guardare a questo. La cosa più facile per me è pensare alla relazione tra "Induttivo" e "Definizioni coinduttive"
Una definizione induttiva di un set è così.
Il set "Nat" è definito come il set più piccolo tale che "Zero" è in Nat, e se n è in Nat "Succ n" è in Nat.
Che corrisponde al seguente Ocaml
type nat = Zero | Succ of nat
Una cosa da notare su questa definizione è che un numero
omega = Succ(omega)
NON è un membro di questo set. Perché? Supponiamo che lo fosse, ora considera l'insieme N che ha tutti gli stessi elementi di Nat tranne che non ha omega. Chiaramente Zero è in N, e se y è in N, Succ (y) è in N, ma N è minore di Nat che è una contraddizione. Quindi, omega non è in Nat.
Oppure, forse più utile per uno scienziato informatico:
Dato un certo set "a", l'insieme "List of a" è definito come il set più piccolo tale che "Nil" è in List of a, e che se xs è in List of a e x è in un "Cons x xs" è in List of a.
Che corrisponde a qualcosa di simile
type 'a list = Nil | Cons of 'a * 'a list
La parola chiave qui è "la più piccola". Se non dicessimo "il più piccolo" non avremmo modo di dire se il set Nat contenesse una banana!
Ancora,
zeros = Cons(Zero,zeros)
non è una definizione valida per un elenco di nats, proprio come omega non era un Nat valido.
La definizione di dati in modo induttivo come questo ci consente di definire le funzioni che funzionano con ricorsione
let rec plus a b = match a with
| Zero -> b
| Succ(c) -> let r = plus c b in Succ(r)
possiamo quindi provare fatti su questo, come "più uno zero = a" usando induzione (in particolare, induzione strutturale)
La nostra prova procede per induzione strutturale su un.
Per il caso base lasciate che sia Zero. plus Zero Zero = match Zero with |Zero -> Zero | Succ(c) -> let r = plus c b in Succ(r)
quindi sappiamo plus Zero Zero = Zero
.
Lascia che a
sia un nat. Assumi l'ipotesi induttiva che plus a Zero = a
. Ora dimostriamo che plus (Succ(a)) Zero = Succ(a)
è ovvio poiché 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)
Quindi, per induzione plus a Zero = a
per tutto a
in nat
Ovviamente possiamo provare cose più interessanti, ma questa è l'idea generale.
Finora abbiamo trattato i dati induttivamente definiti che abbiamo ottenuto lasciando che fosse il set "più piccolo". Quindi ora vogliamo lavorare con codata definito in modo coincertivo, che otteniamo lasciando che sia il set più grande.
Sia un set. Il set "Stream of a" è definito come il più grande set tale che per ogni x nel flusso di a, x consiste nella coppia ordinata (testa, coda) tale che la testa è in a e tail è in Stream of a
In Haskell lo esprimiamo come
data Stream a = Stream a (Stream a) --"data" not "newtype"
In realtà, in Haskell usiamo normalmente gli elenchi integrati, che possono essere una coppia ordinata o una lista vuota.
data [a] = [] | a:[a]
Anche Banana non è un membro di questo tipo, dal momento che non è una coppia ordinata o la lista vuota. Ma ora possiamo dire
ones = 1:ones
e questa è una definizione perfettamente valida. Per di più, possiamo eseguire la ricorsione su questi co-dati. In realtà, è possibile che una funzione sia ricorsiva e ricorsiva. Mentre la ricorsione è stata definita dalla funzione con un dominio costituito da dati, la ricorsione coincide semplicemente con un co-dominio (chiamato anche l'intervallo) che è co-dati . La ricorsione primitiva significava sempre "chiamarsi" su più piccoli dati fino a raggiungere alcuni dati più piccoli. La co-ricorsione primitiva si "chiama sempre" su dati maggiori o uguali a quelli che avevi prima.
ones = 1:ones
è primitivo co-ricorsivo. Mentre la funzione map
(un po 'come "foreach" nelle lingue imperative) è sia primitivamente ricorsiva (sorta di) che primitivamente co-ricorsiva.
map :: (a -> b) -> [a] -> [b]
map f [] = []
map f (x:xs) = (f x):map f xs
lo stesso vale per la funzione zipWith
che prende una funzione e una coppia di liste e le combina insieme usando quella funzione.
zipWith :: (a -> b -> c) -> [a] -> [b] -> [c]
zipWith f (a:as) (b:bs) = (f a b):zipWith f as bs
zipWith _ _ _ = [] --base case
il classico esempio di linguaggi funzionali è la sequenza di Fibonacci
fib 0 = 0
fib 1 = 1
fib n = (fib (n-1)) + (fib (n-2))
che è originariamente ricorsivo, ma può essere espresso in modo più elegante come una lista infinita
fibs = 0:1:zipWith (+) fibs (tail fibs)
fib' n = fibs !! n --the !! is haskell syntax for index at
un interessante esempio di induzione / coinduzione sta dimostrando che queste due definizioni calcolano la stessa cosa. Questo è lasciato come esercizio per il lettore.