关于Haskell中的Church编码列表

时间:2018-06-08 16:59:45

标签: list haskell functional-programming church-encoding

各种优化问题,如this one,导致Church编码列表作为启用流融合的一种方式,即编译器消除中间结果(例如列表)。以下是在优化问题中成功使用的定义:

{-# LANGUAGE RankNTypes #-}

-- A list encoded as a strict left fold.
newtype ListL a = ListL {build :: forall b. (b -> a -> b) -> b -> b}

以下是我对Church-somethings的看法:不要问' 是什么 ,而是询问可以为你做什么。在列表的情况下,答案是:列表可以折叠。为了折叠,我需要b->a->b类型的“更新”函数和类型b的起始值。然后我会回复折叠的结果,类型为b。因此ListL的定义。以下是ListL上的一些基本操作:

mapL :: (a -> a') -> ListL a -> ListL a'
mapL f l = ListL (\f' b' -> build l (\b a -> f' b (f a)) b')

instance Functor ListL where fmap = mapL

fromList :: [a] -> ListL a
fromList l = ListL (\c z -> foldl' c z l)

toList :: ListL a -> [a]
toList l = build l snoc [] where snoc xs x = xs ++ [x]

nullL :: ListL a -> Bool
nullL l = build l (\_ _->False) True

以下是更多信息:

filterL :: (a->Bool) -> ListL a -> ListL a
filterL p l = ListL (\f b->build l (\b' a'->if p a' then f b' a' else b') b)

iterUntil :: (a->Bool) -> a -> (a->a) -> ListL a
iterUntil p a f = ListL (\g b-> snd $ until (p.fst) (\(a',b')->(f a', g b' a')) (a,b))

iterUntil迭代函数a->a,从类型a的某个值开始,直到满足谓词a->bool。像Prelude iterate这样的函数是不可能的 - 至少我不知道如何定义它,它必须是某种递归。

继续举例,lengthsum只是在foldl中选择正确的“更新”功能和起始值的练习:

lengthL :: ListL a -> Int
lengthL l = build l (\b _ -> b+1) 0

sumL :: Num a => ListL a -> a
sumL l = build l (+) 0

现在,让我们试试headL

headL :: ListL a -> a
headL l = build l (\_ a->a) _   -- this does not compile!

无论提供什么开始b,都应返回第一个abuild l需要b,但我们没有b。这是一个奇怪的问题:基本上我们想告诉编译器:你不需要headL' :: ListL a -> ListL a,相信我......另一方面,error "empty list!"很容易构建。代替洞_的{​​{1}}不起作用,因为它总是被调用 - 懒惰似乎没有解决这个问题。所以,headL我被卡住了。因此,这是

问题1:headL如何实施?

尝试实施等效的repeatM :: Monad m => m a -> m [a]时会出现第二个问题。与iterUntil一样,需要使用谓词a->Bool来停止迭代:

iterUntilM :: Monad m => (a->Bool) -> m a -> m (ListL a)

目的很明确:重复一次性行动m a,直到a->Bool满意为止。当然,这个想法是立即折叠ListL a并实现流融合(列表融合)。例如:

import System.Random (randomIO)

main :: IO ()
main = do
     rs <- iterUntilM (>42::Int) randomIO
     print $ lengthL rs

这个例子非常人为,它打印了第一个数字&gt; 42之前的绘制次数。在更现实的设置中,monad m例如是包含一些FFI的ST s monad。关键是:这应该有效运行。我完全坚持这个。如何将(>>=) :: m a -> (a->m b) -> m bbuild纠缠在一起以获得m (ListL a)?即这是

问题2:iterUntilM如何实施?

除了做一个好的学习练习,这实际上是个好主意吗?

1 个答案:

答案 0 :(得分:5)

通常,当您删除有关类型的假设时,您编写的函数不仅更通用(就其可以使用的类型而言),它还将更具有特定关于什么它到底在做什么。这就是教会编码允许融合的原因:当列表表示为

data [a] = [] | a : [a]

在函数中有无数种方法可以使用它们,其中只有一种是foldr。但是,当你有:

newtype List a = { runList :: forall b. (a -> b -> b) -> b -> b }

使用该类型的方式是foldr。这就是让您进行我们所熟知和喜爱的优化的原因。顺便说一下,流融合只是其中之一:例如,你也可以附加 O(1)

你的类型更受限制:它告诉我们基础列表不能(有意义地)无限。

还有一个受限制的代表表示会改变焦点:

data List a = forall b. List b (b -> Maybe (a, b))

教会编码列表是消费者,这是制作者。它没有说明如何使用这个列表,而是关于它如何被制作的非常多。

所以我们已经看到我们从这些受限制的表示中获得了很多,我们失去了什么? tail就是一个很好的例子。对于制作人:

tail (List x f) = case f x of
  Just (_,xs) -> List xs f

对消费者而言:

tail xs =
    List (\c n ->
        runList xs 
            (\h t g -> g h (t c)) 
            (const n) 
            (const id))

消费者的实现是 O(n),而生产者显然是 O(1)

这两种类型都可以允许融合,但某些功能可以更有效地在一个中实现。 GHC碰巧选择前一种表示作为融合的基础,但没有什么根本可以使这种选择变得更好:Haskellers使用的大多数函数似乎在foldr/build融合模式中比其他函数更好地工作一。在other places中,使用了展开模式。

这个序言,我们需要提出两个问题:

  • 这些功能(headiterUntilM)仅在foldr表示(如追加)或unfoldr表示(如tail中有效运作})或两者(如map)?
  • 严格的左折编码是否适合这些?是否过于拘束(即我们需要foldr?),还是可以限制更多?

head可以很容易地在foldr - 编码列表上实现:

head xs = runList xs const (error "head: empty list")

foldl' - 列表中,它有点复杂:

head xs =
    fromMaybe
        (error "head: empty list")
        (build xs (\xs x -> maybe (Just x) Just xs) Nothing)

您会注意到此功能(如tail上的foldr - 列表)是 O(n)。它也不适用于无限列表。这表明foldl'不是融合head的正确选择。

现在,对于iterUntilM,我们看到的情况是(我不认为)甚至 fusion 也是可能的。因为m最终在外面,你必须运行列表中的所有效果(实现它)。

要详细了解此区域,请查看this博文。