更高效的教堂编码列表尾部

时间:2015-08-29 16:40:45

标签: list haskell time-complexity church-encoding scott-encoding

这是一个有文化的haskell帖子。只需将其保存为" 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

基本上我的实现是将列表拆分为头尾。缺点取代头部,将旧头部贴在尾巴上。这是相当低效的。 教会名单似乎在分裂时效率低下。

我希望自己错了。是否有tailCl的实现优于O(n)(最好是O(1))。

3 个答案:

答案 0 :(得分:6)

Koopman,Plasmeijer和Jansen的论文Church Encoding of Data Types Considered Harmful for Implementations似乎在详细讨论这个问题。特别是引用摘要(我的重点):

  

[...]

     

我们在教会编码选择器中显示构造函数   产生递归类型,如列表的 tail ,有一个不合需要的   严格的数据结构的脊柱。斯科特编码   不以任何方式妨碍懒惰的评价。 评价   由教会编码的递归脊椎使得复杂性   这些析构函数 O(n) Scott编码中的相同析构函数   只需要一段时间。而且,教会编码有   图减少的严重问题。 Parigot编码结合起来   两个世界中最好的,但在实践中,这并没有提供   优点

但是,虽然Scott encoding提供了性能优势,但似乎problematic在系统F中定义它而不添加递归类型。

答案 1 :(得分:3)

是的,是O(n)。教会编码列表用其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

答案 2 :(得分:0)

不,不是必然 O(n):

  

前奏>拿5。 snd。 foldr(\ a r->(a:fst r,fst r))([],undefined)$ [1 ..]
  [2,3,4,5,6]

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

尝试 safetail 不起作用:

  

前奏> fmap(取5)。 snd。 foldr(\ a r->(fmap(a :) $ fst r,fst r))(Just [],Nothing)
   $ [1 ..]
  中断。

所以,

tailCL cl = CList $ \k z-> snd $ runCList cl (\a r-> (a`k`fst r,fst r)) (z, undefined)
  

前奏>拿5。 toList。 tailCL。 fromList $ [1 ..]
      [2,3,4,5,6]

编辑:@user3237465跟进评论,结果发现 safetail 是可能的:

  

前奏> fmap(取5)。 snd。 foldr(\ a~(r,_) - >(Just(a:fromJust r),r))(Just []   ,没什么)$ [1 ..]
  只是[2,3,4,5,6]

以前不起作用的原因是Maybe的{​​{1}}强迫其第二个参数找出它是哪种情况,但在这里我们知道它通过构造是fmap值。我不能把它作为你的类型的定义,不管我试过什么都不通过类型检查器。