Esistono diversi modi per esaminarlo. La cosa più semplice per me è pensare alla relazione tra "induttive" e "definizioni induttive"
Una definizione induttiva di un set va così.
Il set "Nat" è definito come il set più piccolo in modo che "Zero" sia 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 per il fatto che non ha omega. Chiaramente Zero è in N, e se y è in N, Succ (y) è in N, ma N è più piccolo di Nat, il che è una contraddizione. Quindi, Omega non è in Nat.
O, forse più utile per un informatico:
Dato un insieme di "a", l'insieme "Elenco di un" è definito come l'insieme più piccolo in modo tale che "Zero" sia nell'Elenco di a e che se xs è nell'Elenco di ae x è in un "Contro x xs" è nella lista di a.
Che corrisponde a qualcosa di simile
type 'a list = Nil | Cons of 'a * 'a list
La parola chiave qui è "il più piccolo". Se non dicessimo "il più piccolo" non avremmo modo di sapere se il set Nat contenesse una banana!
Ancora,
zeros = Cons(Zero,zeros)
non è una definizione valida per un elenco di nat, proprio come omega non era un nat valido.
Definire i dati in modo induttivo in questo modo ci consente di definire funzioni che funzionano su di esso usando la 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 al riguardo, come "più uno Zero = a" usando l' induzione (in particolare l'induzione strutturale)
La nostra prova procede per induzione strutturale su a.
Per il caso base lasciate essere zero. plus Zero Zero = match Zero with |Zero -> Zero | Succ(c) -> let r = plus c b in Succ(r)
quindi lo sappiamo plus Zero Zero = Zero
. Lascia che a
sia un nat. Assumi l'ipotesi induttiva che plus a Zero = a
. Mostriamo ora che plus (Succ(a)) Zero = Succ(a)
questo è ovvio dal momento che 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)
, per induzione plus a Zero = a
per tutti i a
nat
Ovviamente possiamo dimostrare cose più interessanti, ma questa è l'idea generale.
Finora abbiamo trattato dati definiti induttivamente che abbiamo ottenuto lasciando che fosse l'insieme "più piccolo". Quindi ora vogliamo lavorare con codata definiti in modo induttivo che otteniamo lasciando che sia il set più grande.
Così
Lascia che sia un set. L'insieme "Stream di a" è definito come l'insieme più grande in modo tale che per ogni x nello stream di a, x sia costituito dalla coppia ordinata (testa, coda) in modo tale che head sia in a e tail sia in Stream di a
In Haskell lo esprimiamo come
data Stream a = Stream a (Stream a) --"data" not "newtype"
In realtà, in Haskell usiamo normalmente gli elenchi predefiniti, che possono essere una coppia ordinata o un elenco vuoto.
data [a] = [] | a:[a]
La banana non è un membro di questo tipo, poiché non è una coppia ordinata o l'elenco vuoto. Ma ora possiamo dire
ones = 1:ones
e questa è una definizione perfettamente valida. Inoltre, 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 che ha un dominio costituito da dati, la ricorsione ricorre solo a un dominio (chiamato anche intervallo) che è co-dati. La ricorsione primitiva significava sempre "chiamarsi" su dati più piccoli fino a raggiungere alcuni dati più piccoli. La ricorsione ricorsiva primitiva si "chiama sempre" su dati maggiori o uguali a quelli che avevi prima.
ones = 1:ones
è ricorsivamente primitivo. Mentre la funzione map
(una specie di "foreach" nei linguaggi imperativi) è sia ricorsivamente primitiva (una sorta di) sia ricorsivamente primitiva.
map :: (a -> b) -> [a] -> [b]
map f [] = []
map f (x:xs) = (f x):map f xs
lo stesso vale per la funzione zipWith
che accetta 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 è primariamente ricorsivo, ma può essere espresso più elegantemente come un elenco infinito
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.