翻转折叠

时间:2012-10-18 08:17:42

标签: haskell fold

假设一分钟我们认为以下是一个好主意:

data Fold x y = Fold {start :: y, step :: x -> y -> y}

fold :: Fold x y -> [x] -> y

在此方案下,可以通过使用适当的length对象作为参数调用sum来实现foldFold等函数。

现在,假设你想做一些聪明的优化技巧。特别是,假设你想写

unFold :: ([x] -> y) -> Fold x y

规则RULES pragma应该相对容易fold . unFold = id。但有趣的问题是......我们实际上可以实施 unFold吗?

显然,您可以使用RULES来应用任意代码转换,无论它们是否保留代码的原始含义。但你真的可以写一个unFold实现,它实际上做了类型签名所暗示的吗?

4 个答案:

答案 0 :(得分:12)

不,这是不可能的。证明:让

f :: [()] -> Bool
f[] = False
f[()] = False
f _ = True

首先,对于f' = unFold f,我们必须start f' = False,因为当折叠空列表时,我们直接获取起始值。然后,我们必须要求step f' () False = False来实现fold f' [()] = False。但是,在现在评估fold f' [(),()]时,我们只会再次拨打step f' () False,我们必须将其定义为False,导致fold f' [(),()] ≡ False,而f[(),()] ≡ True。因此,没有unFold f满足fold $ unFold f ≡ f。 □

答案 1 :(得分:7)

你可以,但你需要对Fold进行一些修改才能将其删除。

列表中的所有功能都可以表示为折叠,但有时为了实现这一点,需要额外的簿记。假设我们在Fold类型中添加了一个额外的类型参数,该参数会传递这些额外的上下文信息。

data Fold a c r = Fold { _start :: (c, r), _step :: a -> (c,r) -> (c,r) }

现在我们可以实现fold,就像这样

fold :: Fold a c r -> [a] -> r
fold (Fold step start) = snd . foldr step start

现在当我们试图走另一条路时会发生什么?

unFold :: ([a] -> r) -> Fold a c r

c来自哪里?函数是不透明的值,因此很难知道如何检查函数并知道它依赖于哪些上下文信息。所以,让我们作弊。我们将“上下文信息”作为整个列表,因此当我们到达最左边的元素时,我们可以将函数应用于原始列表,忽略先前的累积结果。

unFold :: ([a] -> r) -> Fold a [a] r
unFold f = Fold { _start = ([], f [])
                , _step = \a (c, _r) -> let c' = a:c in (c', f c') }

现在,遗憾的是,这不一定与fold有关,因为它要求c必须是[a]。让我们通过隐藏c与存在量化来解决这个问题。

{-# LANGUAGE ExistentialQuantification #-}
data Fold a r = forall c. Fold
  { _start :: (c,r)
  , _step :: a -> (c,r) -> (c,r) }

fold :: Fold a r -> [a] -> r
fold (Fold start step) = snd . foldr step start

unFold :: ([a] -> r) -> Fold a r
unFold f = Fold start step where
  start = ([], f [])
  step a (c, _r) = let c' = a:c in (c', f c')

现在,fold . unFold = id应始终如此。而且,考虑到Fold数据类型的平等概念,您也可以说unFold . fold = id。您甚至可以提供一个类似于旧Fold构造函数的智能构造函数:

makeFold :: r -> (a -> r -> r) -> Fold a r
makeFold start step = Fold start' step' where
  start' = ((), start)
  step' a ((), r) = ((), step a r)

答案 2 :(得分:6)

TL; DR:

结论1:你不能

你原来要求的是不可能的,至少不是你想要的任何版本。 (见下文。) 如果更改您的数据类型以允许我存储中间计算,我想我会没事的,但即便如此, 函数unFold效率相当低,这似乎与你聪明的优化技巧议程背道而驰!

结论2:即使你通过改变类型来解决它,我认为它不会达到你想要的效果

列表算法的任何优化都会受到使用原始未优化函数计算步长函数的问题的影响,并且可能很复杂。

由于功能上没有相同性,因此无法优化步骤以实现高效。我认为你需要人来做unFold,而不是编译。

无论如何,回到最初的问题:

可折叠。 unFold = id?

没有。假设我们有

isSingleton :: [a] -> Bool
isSingleton [x] = True
isSingleton _ = False

然后,如果我们有unFold :: ([x] -> y) -> Fold x y,那么如果foldSingletonunFold isSingleton相同则需要

foldSingleton = Fold {start = False , step = ???}

其中step采用列表的元素并更新结果。 现在isSingleton "a" == True,我们需要

step False = True

因为isSingleton "ab" == False,我们需要

step True = False

所以step = not会这么做,但isSingleton "abc" == False所以我们还需要

step False = False

由于函数([x] -> y)无法由Fold x y类型的值表示,因此unFold :: ([x] -> y) -> Fold x y不能存在函数fold . unFold = id,因为id是一个完整的功能。


编辑:

事实证明你并不相信这一点,因为你只期望unFold处理具有表示折叠的函数,所以也许你的意思是unFold.fold = id

可以解开。 fold = id?

没有。
即使您只是希望unFold处理可以使用([x] -> y)获得的函数fold :: Fold x y -> ([x] -> y),我认为这是不可能的。让我们假设现在已经定义了

来解决这个问题
combine :: X -> Y -> Y
initial :: Y

folded :: [X] -> Y
folded = fold $ Fold initial combine

恢复值initial非常简单:initial = folded []。 原始combine的恢复不是,因为没有办法从一个为Y提供Y值的函数,而是将X = Y = Int的任意值组合在一起。

例如,如果我们有 combine x y | y < 0 = -10 | otherwise = y + 1 initial = 0 并且我定义了

combine

然后由于y每次在正y上使用folded时只添加一个length,并且初始值为0,folded xs与{{1}无法区分就其输出而言。请注意,由于unFold :: ([x] -> y) -> Fold x y永远不会为负数,因此也无法定义能够恢复combine函数的函数fold。这归结为Fold x y不是单射的事实;它将[x] -> y类型的不同值带到类型unFold :: ([x] -> y) -> Fold x y的相同值。

因此,我证明了两件事:如果fold.unFold /= id,那么unFold.fold /= id和现在Fold 0 (\_ y -> y+1)

我打赌你也不相信,因为你并不关心你是否Fold 0 combineunFold foldedunFold回来,因为它们具有相同的价值什么时候重新折叠!让我们再一次缩小球门柱。也许你希望fold只要能通过unFold获得功能就可以工作,并且你很高兴它不会给你不一致的答案,只要你再次折叠结果,你就会得到相同的功能。我可以用下一个问题总结一下:

可折叠。展开。 fold = fold?

即。您可以定义fold.unFold,以便fold是通过step可获得的功能集上的标识吗?

我确信这是不可能的,因为在不保留有关子列表中间值的额外信息的情况下计算unFold f = Fold {start = f [], step = recoverstep f} 函数不是一个可以解决的问题。

假设我们有

recoverstep f x1 initial == f [x1]

我们需要

recoverstep f x1 y | y == initial = f [x1]

所以,如果有一个x的Eq实例(敲响警钟!),那么recoverstep必须具有与

相同的效果
recoverstep f x2 (f [x1]) == f [x1,x2]

我们也需要

recoverstep f x2 y | y == (f [x1]) = f [x1,x2]

所以如果有一个x的Eq实例,那么recoverstep必须具有与

相同的效果
x1

但这里存在一个大问题:变量f [x1]在此等式的右侧是空闲的。 这意味着从逻辑上讲,除非我们已经知道,否则我们无法分辨步骤函数对x应该具有的值 知道它已经使用了什么值。我们需要在折叠中存储f [x1,x2]unFold等的值 数据类型使它工作,这就是为什么我们无法定义{{1}}的关键。如果更改数据类型折叠 为了让我们能够存储关于中间列表的信息,我可以看到它会起作用,但是它是不可能的 恢复上下文。

答案 3 :(得分:1)

Dan's answer类似,但采用略有不同的方法。我们不是将累加器与将在最后抛出的部分结果配对,而是添加一个“后处理”函数,它将从累加器类型转换为最终结果。

unFold的“欺骗”只是后处理步骤中的所有工作:

{-# LANGUAGE ExistentialQuantification #-}

data Fold a r = forall c. Fold
  { _start  :: c
  , _step   :: a -> c -> c
  , _result :: c -> r }

fold :: Fold a r -> [a] -> r
fold (Fold start step result) = result . foldr step start

unFold :: ([a] -> r) -> Fold a r
unFold f = Fold [] (:) f

makeFold :: r -> (a -> r -> r) -> Fold a r
makeFold start step = Fold start step id