这些有什么区别?
在Wikipedia上,几乎没有信息,也没有清晰的代码来解释这些术语。
有哪些非常简单的示例可以解释这些术语?
corecursion如何实现递归的对偶?
有没有经典的corecusive算法?
这些有什么区别?
在Wikipedia上,几乎没有信息,也没有清晰的代码来解释这些术语。
有哪些非常简单的示例可以解释这些术语?
corecursion如何实现递归的对偶?
有没有经典的corecusive算法?
Answers:
有很多很好的方法可以解决这个问题。对我来说,最简单的方法是考虑“归纳”和“共归定义”之间的关系
集合的归纳定义是这样的。
集合“ Nat”定义为最小集合,以使“零”在Nat中,如果n在Nat中,“ Succ n”在Nat中。
对应于以下Ocaml
type nat = Zero | Succ of nat
关于此定义要注意的一件事是
omega = Succ(omega)
不是该集合的成员。为什么?假设是,现在考虑集合N与元素Nat具有所有相同的元素,只不过它没有omega。显然,零在N中,如果y在N中,则Succ(y)在N中,但是N小于Nat,这是一个矛盾。因此,欧米茄不在Nat中。
或者,对于计算机科学家来说可能更有用:
给定某个集合“ a”,将集合“ a的列表”定义为最小集合,以使“ Nil”在a的列表中,并且如果xs在a的列表中并且x在“ Cons x xs”中在清单a中。
对应于类似的东西
type 'a list = Nil | Cons of 'a * 'a list
这里的操作词是“最小的”。如果我们不说“最小的”,我们将没有任何办法告诉我们这套Nat是否包含香蕉!
再次,
zeros = Cons(Zero,zeros)
不是nat列表的有效定义,就像omega不是有效的Nat。
像这样以归纳方式定义数据,使我们可以使用递归来定义在其上起作用的函数
let rec plus a b = match a with
| Zero -> b
| Succ(c) -> let r = plus c b in Succ(r)
然后我们可以使用归纳法(特别是结构归纳法)证明有关这一事实,例如“加零= a”
我们的证明通过对a的结构归纳来进行。
对于基本情况,将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
的NAT
我们当然可以证明更多有趣的事情,但这是总的想法。
到目前为止,我们已经通过归纳定义数据作为“最小”集来处理数据。因此,现在我们要使用共同定义的协同数据,通过将其设为最大集合来获得协同数据。
所以
让一个集合。集合“ a的流”定义为最大集合,这样对于a的流中的每个x,x都由有序对(头,尾)组成,使得头在a中,而尾在a的流中
在Haskell中,我们将其表示为
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
归纳/共归的一个有趣的例子是证明这两个定义可以计算相同的事物。这留给读者练习。
基本上,corecursion是递归累加器式的,其结果是从起始案例开始的,而常规的递归则是从基础案例返回的结果。
(现在讲Haskell)。这就是为什么foldr
(有严格的组合功能)表示递归,foldl'
(严格的梳子。F。)/ scanl
/ until
/ iterate
/ unfoldr
/等表达corecursion。Corecursion无处不在。foldr
用非严格的梳子。F。表示尾递归模的缺点。
这是递归:
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]
(读$
为“ of”)。这是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
= .....
在Vitomir Kovanovic的博客上进行检查。我发现了这一点:
懒惰求值是编程语言中具有lisp,haskell,python等功能编程功能的一项非常不错的功能。它要求将变量值的求值延迟到该变量的实际使用。
这意味着,例如,如果您要创建一百万个这样的元素的列表,
(defn x (range 1000000))
则实际上并没有创建它,而是仅在第一次使用该变量时(例如,当您想要第10个元素时)指定该列表。该列表解释器仅创建该列表的前10个元素。因此,第一次运行(占用10倍)实际上创建了这些元素,并且对同一函数的所有后续调用都在使用已经存在的元素。这非常有用,因为您可以创建无限列表而不会出现内存不足的错误。当然,如果您的程序正在处理大量数据,则在使用这些无限列表时可能会达到内存限制。
另一方面,corecursion对递归是双重的。这是什么意思 就像递归函数以自身形式表示一样,核心递归变量也以自身形式表示。
最好在示例中表达出来。
假设我们要列出所有素数...