【问题标题】:Why can you reverse list with foldl, but not with foldr in Haskell为什么你可以在 Haskell 中用 foldl 反转列表,但不能用 foldr
【发布时间】:2014-11-18 23:21:43
【问题描述】:

为什么可以用 foldl 反转列表?

reverse' :: [a] -> [a]
reverse' xs = foldl (\acc x-> x : acc) [] xs

但是这个给了我一个编译错误。

reverse' :: [a] -> [a]
reverse' xs = foldr (\acc x-> x : acc) [] xs

错误

Couldn't match expected type `a' with actual type `[a]'
`a' is a rigid type variable bound by
  the type signature for reverse' :: [a] -> [a] at foldl.hs:33:13
Relevant bindings include
x :: [a] (bound at foldl.hs:34:27)
acc :: [a] (bound at foldl.hs:34:23)
xs :: [a] (bound at foldl.hs:34:10)
reverse' :: [a] -> [a] (bound at foldl.hs:34:1)
In the first argument of `(:)', namely `x'
In the expression: x : acc

【问题讨论】:

  • 您应该使用++ 并将acc 包装在一个列表中:\acc x -> x ++ [acc]foldr 类型是 (a -> b -> b) -> b -> [a] -> b,所以第一个参数是您正在考虑的“当前元素”(而不是真正的累加器),第二个参数将包含累加器。最好写成:\x acc -> acc ++ [x].

标签: haskell fold


【解决方案1】:

每个foldl 都是foldr

让我们记住定义。

foldr :: (a -> s -> s) -> s -> [a] -> s
foldr f s []       = s
foldr f s (a : as) = f a (foldr f s as)

这是列表的标准问题一步迭代器。我曾经让我的学生敲打桌子高呼“你怎么处理空列表?你怎么处理a : as”?这就是你如何弄清楚sf 分别是什么。

如果您考虑正在发生的事情,您会发现foldr 有效地计算了f a 函数的大量组合,然后将该组合应用于s

foldr f s [1, 2, 3]
= f 1 . f 2 . f 3 . id $ s

现在,让我们看看foldl

foldl :: (t -> a -> t) -> t -> [a] -> t
foldl g t []       = t
foldl g t (a : as) = foldl g (g t a) as

这也是对列表的一步迭代,但累加器会随着我们的进行而变化。让我们最后移动它,以便列表参数左侧的所有内容保持不变。

flip . foldl :: (t -> a -> t) -> [a] -> t -> t
flip (foldl g) []       t = t
flip (foldl g) (a : as) t = flip (foldl g) as (g t a)

现在如果我们将= 向左移动一位,我们可以看到一步迭代。

flip . foldl :: (t -> a -> t) -> [a] -> t -> t
flip (foldl g) []       = \ t -> t
flip (foldl g) (a : as) = \ t -> flip (foldl g) as (g t a)

在每种情况下,我们计算如果我们知道累加器会做什么,抽象为\ t ->。对于[],我们将返回t。对于a : as,我们将使用g t a 作为累加器处理尾部。

但现在我们可以将flip (foldl g) 转换为foldr。抽象出递归调用。

flip . foldl :: (t -> a -> t) -> [a] -> t -> t
flip (foldl g) []       = \ t -> t
flip (foldl g) (a : as) = \ t -> s (g t a)
  where s = flip (foldl g) as

现在我们可以将其转换为 foldr,其中类型 st -> t 实例化。

flip . foldl :: (t -> a -> t) -> [a] -> t -> t
flip (foldl g) = foldr (\ a s -> \ t -> s (g t a)) (\ t -> t)

所以s 说“as 会对累加器做什么”,我们会返回 \ t -> s (g t a),即“a : as 对累加器做什么”。向后翻转。

foldl :: (t -> a -> t) -> t -> [a] -> t
foldl g = flip (foldr (\ a s -> \ t -> s (g t a)) (\ t -> t))

Eta-展开。

foldl :: (t -> a -> t) -> t -> [a] -> t
foldl g t as = flip (foldr (\ a s -> \ t -> s (g t a)) (\ t -> t)) t as

减少flip

foldl :: (t -> a -> t) -> t -> [a] -> t
foldl g t as = foldr (\ a s -> \ t -> s (g t a)) (\ t -> t) as t

所以我们计算“如果我们知道累加器我们会做什么”,然后我们将初始累加器提供给它。

将球打低一点是有一定指导意义的。我们可以摆脱\ t ->

foldl :: (t -> a -> t) -> t -> [a] -> t
foldl g t as = foldr (\ a s -> s . (`g` a)) id as t

现在让我使用 >>>Control.Arrow 反转该组合。

foldl :: (t -> a -> t) -> t -> [a] -> t
foldl g t as = foldr (\ a s -> (`g` a) >>> s) id as t

也就是说,foldl 计算一个大的reverse 组合。因此,例如,给定[1,2,3],我们得到

foldr (\ a s -> (`g` a) >>> s) id [1,2,3] t
= ((`g` 1) >>> (`g` 2) >>> (`g` 3) >>> id) t

“管道”从左侧输入它的参数,所以我们得到

((`g` 1) >>> (`g` 2) >>> (`g` 3) >>> id) t
= ((`g` 2) >>> (`g` 3) >>> id) (g t 1)
= ((`g` 3) >>> id) (g (g t 1) 2)
= id (g (g (g t 1) 2) 3)
= g (g (g t 1) 2) 3

如果你选择g = flip (:)t = [],你会得到

flip (:) (flip (:) (flip (:) [] 1) 2) 3
= flip (:) (flip (:) (1 : []) 2) 3
= flip (:) (2 : 1 : []) 3
= 3 : 2 : 1 : []
= [3, 2, 1]

也就是说,

reverse as = foldr (\ a s -> (a :) >>> s) id as []

通过将foldlfoldr一般 转换实例化。

仅适用于数学家。 执行 cabal install newtype 并导入 Data.MonoidData.FoldableControl.Newtype。添加悲惨丢失的实例:

instance Newtype (Dual o) o where
  pack = Dual
  unpack = getDual

观察到,一方面,我们可以通过foldr 实现foldMap

foldMap :: Monoid x => (a -> x) -> [a] -> x
foldMap f = foldr (mappend . f) mempty

反之亦然

foldr :: (a -> b -> b) -> b -> [a] -> b
foldr f = flip (ala' Endo foldMap f)

这样foldr 在组成内函数的幺半群中累积,但现在要得到foldl,我们告诉foldMapDual 中工作。

foldl :: (b -> a -> b) -> b -> [a] -> b
foldl g = flip (ala' Endo (ala' Dual foldMap) (flip g))

mappend 对应于 Dual (Endo b) 是什么?模换行,正好是逆向组合,>>>

【讨论】:

【解决方案2】:

可以使用foldr 有效地反转列表(嗯,大部分时间在 GHC 7.9 中——它依赖于一些编译器优化),但这有点奇怪:

reverse xs = foldr (\x k -> \acc -> k (x:acc)) id xs []

我写了一个解释on the Haskell Wiki这是如何工作的。

【讨论】:

    【解决方案3】:

    其中几个答案的一个轻微但重要的概括是,您可以使用foldr 实现foldl,我认为这是解释其中发生了什么的更清晰的方式:

    myMap :: (a -> b) -> [a] -> [b]
    myMap f = foldr step []
        where step a bs = f a : bs
    
    -- To fold from the left, we:
    --
    -- 1. Map each list element to an *endomorphism* (a function from one
    --    type to itself; in this case, the type is `b`);
    --
    -- 2. Take the "flipped" (left-to-right) composition of these
    --    functions;
    --
    -- 3. Apply the resulting function to the `z` argument.
    --
    myfoldl :: (b -> a -> b) -> b -> [a] -> b
    myfoldl f z as = foldr (flip (.)) id (toEndos f as) z
        where
          toEndos :: (b -> a -> b) -> [a] -> [b -> b]
          toEndos f = myMap (flip f)
    
    myReverse :: [a] -> [a]
    myReverse = myfoldl (flip (:)) []
    

    关于这里的想法的更多解释,我建议阅读 Tom Ellis 的 "What is foldr made of?" 和 Brent Yorgey 的 "foldr is made of monoids"

    【讨论】:

      【解决方案4】:

      这就是 foldl op acc 对包含 6 个元素的列表所做的:

      (((((acc `op` x1) `op` x2) `op` x3) `op` x4) `op` x5 ) `op` x6
      

      foldr op acc 这样做:

      x1 `op` (x2 `op` (x3 `op` (x4 `op` (x5 `op` (x6 `op` acc)))))
      

      当您查看此内容时,很明显如果您希望 foldl 反转列表,op 应该是“将右操作数粘贴到左操作数的开头”运算符。这只是(:),参数颠倒了,即

      reverse' = foldl (flip (:)) []
      

      (这与您的版本相同,但使用内置函数)。

      当您希望foldr 反转列表时,您需要一个“将左操作数粘贴到右操作数末尾”的运算符。我不知道这样做的内置函数。如果你愿意,你可以写成flip (++) . return

      reverse'' = foldr (flip (++) . return) []
      

      或者如果你更喜欢自己写

      reverse'' = foldr (\x acc -> acc ++ [x]) []
      

      不过这会很慢。

      【讨论】:

        【解决方案5】:

        foldr 基本上以规范的方式解构一个列表:foldr f initial 与具有模式的函数相同:(这基本上是 foldr 的 定义

         ff [] = initial
         ff (x:xs) = f x $ ff xs
        

        即它un-conses元素一个一个地提供给f。好吧,如果f 所做的只是再次将他们骗回来,那么您将获得最初拥有的列表! (另一种说法:foldr (:) [] ≡ id

        foldl 以相反的顺序“解构”列表,因此如果您将元素反向,则会得到反向列表。要使用foldr 获得相同的结果,您需要附加到“错误”的结尾——正如 MathematicalOrchid 所示,++ 效率低下,或者使用差异列表

        reverse'' :: [a] -> [a]
        reverse'' l = dl2list $ foldr (\x accDL -> accDL ++. (x:)) empty l
        
        type DList a = [a]->[a]
        (++.) :: DList a -> DList a -> DList a
        (++.) = (.)
        emptyDL :: DList a
        emptyDL = id
        dl2list :: DLList a -> [a]
        dl2list = ($[])
        

        可以简写为

        reverse''' l = foldr (flip(.) . (:)) id l []
        

        【讨论】:

        • 这还不够明确:) reverse'''' = foldr (flip(.) . (:)) id ``flip`` []
        • @SassaNF: reverse''''' = flip ($) [] . flip id id (flip id (flip (.) (:) $ flip fmap) foldr)
        • 我想知道,reverse''(++.) a b c = let z=b c in z `seq` a z 的性能会更好吗?
        【解决方案6】:

        首先,类型签名不对齐:

        foldl :: (o -> i -> o) -> o -> [i] -> o
        foldr :: (i -> o -> o) -> o -> [i] -> o
        

        因此,如果您交换参数名称:

        reverse' xs = foldr (\ x acc -> x : acc) [] xs
        

        现在它可以编译了。它不会工作,但现在可以编译了。

        问题是,foldl 从左到右(即向后)工作,而 foldr 从右到左工作(即向前)。这就是为什么foldl 可以让您反转列表的原因;它以相反的顺序递给你。

        说了这么多,你可以

        reverse' xs = foldr (\ x acc -> acc ++ [x]) [] xs
        

        不过会真的很慢。 (二次复杂度而不是线性复杂度。)

        【讨论】:

        • 你可以让它在线性时间内与休斯列表一起工作(如 showS)
        • 您可以使用差异列表通过foldr 有效地反转列表。
        猜你喜欢
        • 2015-02-25
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2015-01-08
        • 1970-01-01
        • 2016-09-04
        • 1970-01-01
        相关资源
        最近更新 更多