各种优化问题,如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
这样的函数是不可能的 - 至少我不知道如何定义它,它必须是某种递归。
继续举例,length
和sum
只是在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
,都应返回第一个a
。 build 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 b
与build
纠缠在一起以获得m (ListL a)
?即这是
问题2:iterUntilM
如何实施?
除了做一个好的学习练习,这实际上是个好主意吗?
答案 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中,使用了展开模式。
这个序言,我们需要提出两个问题:
head
和iterUntilM
)仅在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博文。