Есть несколько хороших способов посмотреть на это. Самая легкая вещь для меня - подумать об отношении между «индуктивными» и «коиндуктивными определениями»
Индуктивное определение множества выглядит следующим образом.
Набор «Nat» определяется как наименьший набор, такой, что «Ноль» находится в Nat, а если n - в Nat, «Succ n» - в Nat.
Что соответствует следующему Ocaml
type nat = Zero | Succ of nat
Об этом определении следует отметить, что число
omega = Succ(omega)
НЕ является членом этого набора. Почему? Предположим, что это так, теперь рассмотрим множество N, которое имеет все те же элементы, что и Nat, за исключением того, что оно не содержит омеги. Ясно, что ноль находится в N, и если y находится в N, Succ (y) находится в N, но N меньше, чем Nat, что является противоречием. Итак, омега не в нат.
Или, может быть, более полезным для ученого:
При некотором наборе «a», набор «List of a» определяется как наименьший набор, такой, что «Nil» находится в List of a, и что, если xs находится в List of a, а x находится в «Cons x xs» находится в списке.
Что соответствует чему-то вроде
type 'a list = Nil | Cons of 'a * 'a list
Оперативное слово здесь «самое маленькое». Если бы мы не сказали «самый маленький», у нас не было бы никакого способа сказать, содержал ли банан Nat набор!
Очередной раз,
zeros = Cons(Zero,zeros)
не является допустимым определением для списка натс, так же как омега не был действительным нат.
Такое индуктивное определение данных позволяет нам определять функции, которые работают с ними, используя рекурсию
let rec plus a b = match a with
| Zero -> b
| Succ(c) -> let r = plus c b in Succ(r)
затем мы можем доказать факты об этом, например, «плюс ноль = а», используя индукцию (в частности, структурную индукцию)
Наше доказательство проводится структурной индукцией по a.
Для базового случая пусть будет ноль. plus Zero Zero = match Zero with |Zero -> Zero | Succ(c) -> let r = plus c b in Succ(r)
Итак, мы знаем plus Zero Zero = Zero
. Позвольте a
быть нац. Предположим, что индуктивная гипотеза plus a Zero = a
. Покажем теперь, что plus (Succ(a)) Zero = Succ(a)
это очевидно, так как 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)
по индукции plus a Zero = a
для всех a
в
Конечно, мы можем доказать более интересные вещи, но это общая идея.
До сих пор мы имели дело с индуктивно определенными данными, которые мы получили, позволив им быть «наименьшим» набором. Итак, теперь мы хотим работать с кодированными кодами, которые мы получаем, позволяя получить самый большой набор.
Так
Позвольте быть набором. Набор «Поток a» определяется как наибольший набор, такой, что для каждого x в потоке a, x состоит из упорядоченной пары (голова, хвост), так что голова находится в a, а хвост находится в потоке a
В Хаскеле мы бы выразили это как
data Stream a = Stream a (Stream a) --"data" not "newtype"
На самом деле, в Haskell мы обычно используем встроенные списки, которые могут быть упорядоченной парой или пустым списком.
data [a] = [] | a:[a]
Банан также не является членом этого типа, так как это не упорядоченная пара или пустой список. Но теперь мы можем сказать,
ones = 1:ones
и это совершенно правильное определение. Более того, мы можем выполнить совместную рекурсию по этим данным. На самом деле, функция может быть одновременно и рекурсивной, и рекурсивной. В то время как рекурсия была определена функцией, имеющей домен, состоящий из данных, ко-рекурсия просто означает, что у нее есть совместная область (также называемая диапазоном), которая является совместной информацией. Примитивная рекурсия означала всегда «звонить самому себе» по меньшим данным до достижения самых маленьких данных. Примитивная совместная рекурсия всегда "вызывает себя" на данных, которые больше или равны тем, которые вы имели ранее.
ones = 1:ones
примитивно ко-рекурсивный. В то время как функция map
(вроде как «foreach» в императивных языках) является примитивно рекурсивной (как бы) и примитивно ко-рекурсивной.
map :: (a -> b) -> [a] -> [b]
map f [] = []
map f (x:xs) = (f x):map f xs
То же самое касается функции, zipWith
которая берет функцию и пару списков и объединяет их вместе, используя эту функцию.
zipWith :: (a -> b -> c) -> [a] -> [b] -> [c]
zipWith f (a:as) (b:bs) = (f a b):zipWith f as bs
zipWith _ _ _ = [] --base case
классическим примером функциональных языков является последовательность Фибоначчи
fib 0 = 0
fib 1 = 1
fib n = (fib (n-1)) + (fib (n-2))
который является примитивно рекурсивным, но может быть более элегантно выражен как бесконечный список
fibs = 0:1:zipWith (+) fibs (tail fibs)
fib' n = fibs !! n --the !! is haskell syntax for index at
Интересным примером индукции / соиндукции является доказательство того, что эти два определения вычисляют одно и то же. Это оставлено в качестве упражнения для читателя.