如何高效地将归纳类型转换为非递归的余归纳类型?(无需使用递归)

15
> {-# LANGUAGE DeriveFunctor, Rank2Types, ExistentialQuantification #-}

任何归纳类型的定义如下所示

> newtype Ind f = Ind {flipinduct :: forall r. (f r -> r) -> r}
> induction = flip flipinduct

归纳法的类型为(f a -> a) -> Ind f -> a。与此相对的是一个称为共归纳的概念。

> data CoInd f = forall r. Coinduction (r -> f r) r
> coinduction = Coinduction

coinduction的类型是(a -> f a) -> a -> CoInd f。注意,inductioncoinduction是对偶的。作为归纳和余归纳数据类型的示例,请看这个函子。

> data StringF rec = Nil | Cons Char rec deriving Functor

没有递归,Ind StringF是一个有限的字符串,CoInd StringF是一个有限或无限的字符串(如果我们使用递归,它们都可以是有限的、无限的或未定义的字符串)。一般来说,可以将任何Functor f转换为Ind f -> CoInd f。例如,我们可以将一个函子值包装在一个共同归纳类型中。

> wrap :: (Functor f) => f (CoInd f) -> CoInd f
> wrap fc = coinduction igo Nothing where
>   --igo :: Maybe (CoInd f) -> f (Maybe (CoInd f))
>   igo Nothing  = fmap Just fc
>   igo (Just (Coinduction step seed)) = fmap (Just . Coinduction step) $ step seed

这个操作会在每个步骤中添加一个额外的操作(模式匹配Maybe),这意味着它会增加O(n)的开销。

我们可以对Ind fwrap进行归纳来得到CoInd f

> convert :: (Functor f) => Ind f -> CoInd f
> convert = induction wrap

这是 O(n^2)。 (获取第一层是O(1),但由于嵌套的Maybe,第n个元素是O(n),使得总复杂度为O(n^2)。)

同样地,我们可以定义cowrap,它接受归纳类型,并显示其顶部函子层。

> cowrap :: (Functor f) => Ind f -> f (Ind f)
> cowrap = induction igo where
>    --igo :: f (f (Ind f)) -> f (Ind f)
>    igo = fmap (\fInd -> Ind $ \fold -> fold $ fmap (`flipinduct` fold) fInd)

归纳法的时间复杂度始终为O(n),因此cowrap的时间复杂度也是O(n)

我们可以使用协同归纳法cowrap归纳法产生CoInd f

> convert' :: (Functor f) => Ind f -> CoInd f
> convert' = coinduction cowrap

每当我们获取一个元素,这又是 O(n) 的操作, 总共需要进行 O(n^2) 次.


我的问题是,不使用递归(直接或间接),我们能否在 O(n) 的时间内将 Ind f 转换为 CoInd f

我知道如何使用递归来完成此操作(将 Ind f 转换为 Fix f 然后再将 Fix f 转换为 CoInd f (初始转换的复杂度为 O(n),但却是每个来自 CoInd f 的元素都是 O(1), 使得第二次转换总时间复杂度为 O(n),而 O(n) + O(n) = O(n) )),但我想看看是否有没有递归的方法。

请注意,convertconvert' 从未使用递归,直接或间接地。真是巧妙,不是吗!


3
@rampion the outer -> is in the meta theory. 给定任何f-代数 f :: f a -> acata f :: Mu f -> a 是从初始f-代数(Mu f)到a的唯一的f-代数同态。这就是你得到 (f a -> a) -> Mu f -> a 的方式。将其翻转并可将其作为Mu f的定义!关于ana / coinduction,给定一个f-余代数(f :: a -> f a),我们有 ana f :: a -> Nu f。因此,(a -> f a) -> a -> Nu f。被翻转的箭头是'f'和cata f / ana f箭头,而不是元理论中的箭头。当你对单子的定义进行对偶以构造余单子时,同样的事情会发生。 - Edward Kmett
Edward KMETT:谢谢! - rampion
我有一种奇怪的感觉,如果可能的话,它将使用与“双重反转”技术相关的某些内容,在functor中隐藏某种种子。而且这可能仅适用于某些functor,如果有可能的话。 - dfeuer
@dfeuer,你有“双重翻转”技术的链接吗? - PyRulez
@PyRulez,我说得非常模糊,请不要字面理解。我在谈论一个尾递归实现的 map,它是这样的:map = reverse . map' [] where {map' seed [] = seed; map' seed (x : xs) = map' (x : seed) xs} - dfeuer
显示剩余2条评论
1个回答

1

是的,这在形式上是可能的:

https://github.com/jyp/ControlledFusion/blob/master/Control/FixPoints.hs

然而,转换仍需要构建一个中间缓冲区,这只能在运行时使用循环来完成。
这种限制的原因是,“归纳”类型的值对给定的评估顺序(*)做出响应,而“共归纳”类型的值则固定了评估顺序。使转换成为可能的唯一方法而不强制进行许多重新计算是分配某种中间缓冲区,以记忆中间结果。
顺便说一下,从“共归纳”到“归纳”的转换不需要缓冲区,但需要通过使用显式循环来重现评估顺序。
顺便说一下,我已经研究了两篇论文中的基本概念: 1. 对于具有效果的流,在Haskell中:https://gist.github.com/jyp/fadd6e8a2a0aa98ae94d 2. 在经典线性逻辑中,用于数组和流。http://lopezjuan.com/limestone/vectorcomp.pdf

(*) 这是在假定使用严格语言的情况下。在存在惰性求值的情况下,情况会有所改变,但概念和最终答案都是相同的。源代码中有一些关于惰性求值的注释。


2
你能解释一下为什么需要一般递归(在这里以Fix的形式)来构建和拆分该缓冲区吗? - dfeuer
@dfeuer:这是因为Haskell 具体递归结构只能使用一般递归声明。 - jyp
有没有循环的双重形式?(我喜欢它,因为即使它使用了一般递归,它也不使用递归类型(具有讽刺意味的是,在非递归环境中根本无法定义此函数,因为我知道一种使用它来生成“Void”的方法。)) - PyRulez
据我所知,“loop”的对偶词是“alloc”。 @PyRulez - jyp

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接