List 更有效的church尾部编码列表

List 更有效的church尾部编码列表,list,haskell,time-complexity,church-encoding,scott-encoding,List,Haskell,Time Complexity,Church Encoding,Scott Encoding,这是一篇哈斯克尔的博文。只需将其保存为“ChurchList.lhs”即可运行它 > {-# LANGUAGE Rank2Types #-} Church编码列表是通过函数表示列表的一种方式。它既类似于折叠风格,也类似于连续传递风格 > newtype ChurchList a = CList {runCList :: forall r. (a -> r -> r) -> r -> r} 为了说明这如何对应于一个列表,这里有一个O(n)同构 > fr

这是一篇哈斯克尔的博文。只需将其保存为“ChurchList.lhs”即可运行它

> {-# LANGUAGE Rank2Types #-}
Church编码列表是通过函数表示列表的一种方式。它既类似于折叠风格,也类似于连续传递风格

> newtype ChurchList a = CList {runCList :: forall r. (a -> r -> r) -> r -> r}
为了说明这如何对应于一个列表,这里有一个O(n)同构

> fromList :: [a] -> ChurchList a
> fromList xs = CList $ \cons empty -> foldr cons empty xs

> toList :: ChurchList a -> [a]
> toList cl = runCList cl (:) []

> instance Show a => Show (ChurchList a) where
>   show cl = "fromList " ++ show (toList cl)
这些东西有很好的性能特点

> singleton :: a -> ChurchList a -- O(1)
> singleton a = CList $ \cons empty -> a `cons` empty
> append :: ChurchList a -> ChurchList a -> ChurchList a -- O(1)!!! This also means cons and snoc are O(1)
> append cl1 cl2 = CList $ \cons empty -> runCList cl1 cons (runCList cl2 cons empty)
> concatCl :: ChurchList (ChurchList a) -> ChurchList a -- O(n)
> concatCl clcl = CList $ \cons empty -> runCList clcl (\cl r -> runCList cl cons r) empty
> headCl :: ChurchList a -> Maybe a -- O(1)
> headCl cl = runCList cl (\a _ -> Just a) Nothing
现在,问题出现在
tail

> tailClbad :: ChurchList a -> Maybe (ChurchList a) --O(n)?!!
> tailClbad cl = (fmap snd) $ runCList cl
>
>    (\a r -> Just (a, case r of
>    Nothing -> CList $ \cons empty -> empty
>    Just (s,t) -> append (singleton s) t)) --Cons
>
>    Nothing --Empty
基本上,我的实现所做的是将列表拆分为头尾两部分。Cons替换头部,并将旧头部附加到尾部。这是相当低效的。 看来,教会名单通常在拆分方面效率低下

我希望我错了。是否有比O(n)(最好是O(1))更好的
tailCl
实现。

是的,它是O(n)。church编码的列表由其foldr函数标识,该函数在任何地方都必须执行相同的操作。由于获取尾部需要对第一项执行某些操作,因此必须对所有剩余项执行相同的操作

{-# LANGUAGE RankNTypes #-}

newtype ChurchList a = CList { getFoldr :: forall r. (a -> r -> r) -> r -> r }

fromList :: [a] -> ChurchList a 
fromList xs = CList $ \f z -> foldr f z xs

toList :: ChurchList a -> [a]
toList cl = getFoldr cl ((:)) []
您的解决方案尽可能高效。同样的解决方案也可以通过构建列表并匹配第一项来编写

safeTail :: [a] -> Maybe [a]
safeTail []     = Nothing
safeTail (_:xs) = Just xs

tailCtrivial ::  ChurchList a -> Maybe (ChurchList a)
tailCtrivial = fmap fromList . safeTail . toList
Koopman、Plasmeijer和Jansen的论文似乎详细论述了这个问题。特别是引用摘要(我的重点):

[……]

我们证明了在Church编码中,构造函数的选择器 产生递归类型,如列表的尾部,会产生不良后果 数据结构脊椎的严格性。斯科特编码 不会以任何方式妨碍惰性评估对 Church编码的递归脊椎使得 这些析构函数是O(n)。斯科特编码中的相同析构函数 只需要恒定的时间。此外,教会的编码方式也有所改变 严重的图形缩减问题。Parigot编码组合 这两个世界都是最好的,但实际上这并不能提供一个 优势

但是,虽然提供了性能优势,但似乎可以在System F中定义它,而无需添加递归类型。

否,不一定是O(n):

前奏曲>第五步。snd。foldr(\a r->(a:fst r,fst r))([],未定义)$[1..]
[2,3,4,5,6]

它确实为最终生成的每个元素增加了O(1)开销

尝试使用
safetail
无效:

前奏曲>fmap(第5步)。snd。foldr(\a r->(fmap(a:)$fst r,fst r))(仅[],无任何内容)
$[1..]
打断了

所以

前奏曲>第五步。托利斯特。泰尔克尔。fromList$[1..]
[2,3,4,5,6]


编辑:在注释之后,结果表明,
safetail
毕竟是可能的:

前奏曲>fmap(第5步)。snd。foldr(\a~(r,)->(Just(a:fromJust r),r))(Just[] ,无任何内容)$[1..]
只是[2,3,4,5,6]


它以前不起作用的原因是,
可能
fmap
强制它的第二个参数找出它是哪种情况,但这里我们知道,通过构造,它只是一个
值。但是我不能把它作为你的类型的定义,不管我做了什么,都没有通过类型检查

正如Oleg Kiselyov所说,这实际上是Boehm Berarducci编码。他在邮件中链接的那篇文章很有意思。而且,斯科特编码似乎没有固定的时间
附加
,这是我对教会列表的主要吸引力。@PyRulez确实如此。似乎在能够在头部进行模式匹配和接近末端之间存在一种权衡。一种解决方案是使用更复杂的数据结构,如所述的可分类FIFO队列(或出队列),这是您可以使用的吗?@PyRulez,Parigot编码允许模式匹配和O(1)附加。这是一个@user3237465,Parigot编码不是完全同构的,因为Church和Scott部分可能会不同步。实际上,您链接的代码将创建不一致的列表。(试着附加两份清单,然后反复地记下它的尾部。)@PetrPudlák我查看教会清单主要是出于理论目的。不过,这对
newtypechurchlistma=CList{runCList::forall r.(a->mr->mr)->mr->mr}
不起作用,因为
m
是一个严格的单子。一种分割数据结构的实际方法,但只有外星人才能理解代码。顺便说一句,
tail'=snd。foldr(\x~(r,-)>(x:r,r))([],未定义)
对我来说更具启发性。@user3237465无论出于何种原因,我都特别想避免这种懒惰模式。
tailCL cl = CList $ \k z-> snd $ runCList cl (\a r-> (a`k`fst r,fst r)) (z, undefined)