【问题标题】:Why is the result of my foldr call on an empty list not in the correct order?为什么我对空列表的 foldr 调用的结果顺序不正确?
【发布时间】:2022-02-06 22:04:42
【问题描述】:

我正在尝试完成 Haskell 九十九题中的第 8 题,但是我在理解为什么我的函数的列表结果排序错误时遇到了问题。

compress 函数的目的是从输入列表中消除任何重复的字母,并按照它们在输入列表中首次出现的顺序输出包含唯一字母的另一个列表。 这是我的压缩功能代码:

compress l = foldr f [] l where f a b = if a `elem` b then b else a : b

当重复的字母彼此相邻时,它可以正常工作,因此 \"aaaabbb\" 输出 \"ab\" 这是正确的,但是当重复的字母被另一个字母分隔时,它会更改其在输出中的顺序,因此 \"aba\"输出 \"ba\" 而预期输出是 \"ab\"。

即使在为 foldr 写出堆栈跟踪时,我似乎也得到了预期的输出,但是当在 GHCI 中使用诸如 \"aba\" 或 \"abca\" 之类的输入运行代码时,我得到了不正确的结果。是什么导致了这种行为?为什么当重复的字母被不同的字母分隔时,输出的顺序会改变?

  • b尾巴被折叠,所以 a `elem` b 只对最后的在字符串中出现a。使用\"aaaabbb\",它仍然是添加到结果中的每个字符的最后一次出现;最后一次出现恰好与第一次出现在同一个“集群”中。
  • 一个低效的剩下有效的折叠将是compress = foldl (\\acc x -> if elem x acc then acc else acc ++ [x]) []。请参阅stackoverflow.com/questions/6172004/writing-foldl-using-foldrcompressfoldl 转换为foldr
  • @chepner,虽然该技术是正确的技术,但该代码不是正确的代码。这里要做的是首先编写一个高效的函数(使用递归助手),然后转换到(右)弃牌。 foldl 的形状不正确,无法很好地做到这一点。
  • 我发布“错误”代码的部分原因是为了避免为 OP 编写正确的代码。这只是为了演示如何处理列表的“结束”。
  • 有这个old SO answer 关于消除列表中的重复项。如果游戏规则禁止使用Set 或者你没有Ord 实例,你总是可以使用另一个列表而不是Set 作为计算状态。

标签: haskell functional-programming fold


【解决方案1】:

foldr 函数以一个初始值(b 类型)和一个Foldable 容器t 开始。对于容器中的“每个”a 值,它使用a 值和“当前”b 值调用函数(a -> b -> b)

Prelude> :t foldr
foldr :: Foldable t => (a -> b -> b) -> b -> t a -> b

compress中,初始值为[],这也使得编译器能够推断出Foldable实例为[]

现在尝试 GHCi 中的步骤。定义(仅用于 GHCi 会话)f 作为顶级函数:

Prelude> f a b = if a `elem` b then b else a : b

如果输入为"aba",则第一次调用fb 的值为[]a 的值为'a',因为foldr 从右侧折叠。

Prelude> f 'a' []
"a"

返回值"a" 现在成为下一次累加器值b

Prelude> f 'b' "a"
"ba"

这是因为f'b' 转换为"a"

累加器值现在是"ba"。再次将其传递给f,使用列表中的第三个也是最后一个值:

Prelude> f 'a' "ba"
"ba"

参见例如this other answer 概述了探索和“调试”Haskell 函数的交互式方式。

【讨论】:

  • 这很有趣。 “第三和最后”绝对是惯用的,但在这种情况下,它可能有点误导。它在时间上是最终的,但在语义上是初始的——它是列表中被折叠的第一个元素!尤其是“列表中的最终值”的延续使得语义解释看起来像是有意的。
  • @DanielWagner 你是对的。当我写那个时,我正在考虑元素的评估顺序。
  • Foldable 实例是 [],而不是 [] Char。我知道你所说的那句话是什么意思,但 OP 可能不会理解你的速记。
  • @dfeuer 好吧,我可以编辑(已编辑)。
  • @Pzet 尝试遍历f 的步骤,如上所示,但使用"ab" 而不是"aba" 作为输入。
【解决方案2】:
compress l = foldr f [] l where f a b = if a `elem` b then b else a : b

... "aba" 输出 "ba" 而预期输出是 "ab"

列表中的foldr 非常简单。它被定义为

foldr g z [a,b,c,...,n]  =  g a (foldr g z [b,c,...,n])

-- and by generalization,
foldr g z [          n]  =  g n z

-- which means
foldr g z [           ]  =      z

这就是它的全部。看起来不言自明,不是吗?只需在语法上重新编写您的 foldr 调用,即可立即查看确切地这是怎么回事。特别是,稍微重写你的定义,我们有

compress xs  =  foldr f [] xs
    where
    f a r  =  if  elem a r  then      r 
                            else  a : r

(我确实(尝试)总是调用组合函数的第二个参数“r“, 为了 ”递归计算出来的结果为了休息输入列表”。)

因此我们有

compress             [a,b,c,...,n]
   =  foldr f []     [a,b,c,...,n]  
   =  f a ( foldr f [] [b,c,...,n] )
   =  if  elem a r  then      r 
                    else  a : r
        where 
        r = foldr f [] [b,c,...,n]
   =  if  elem a r  then      r 
                    else  a : r
        where 
        r = compress   [b,c,...,n]

(使用where 就好像它是表达式的一部分,例如“翻转let”)。换句话说,

compress (x:xs) = if  elem x   r  
                    then       r
                    else   x : r
                  where        r = compress xs
compress []     = []

内容如下:“如果x 出现在压缩的其余的输入,跳过它;否则,保留它;并继续压缩休息”。 (所以,称它为b 具有误导性;暗示ab 是相似的实体;它们是不是--a(或x)是一个元素列表的,r 是它的转换尾巴)。

所以你看到问题出在哪里:如果列表中有多个as,则此定义保留最后的一个,而您想保留第一个。

因此,如果连续有几个,则这种差异是无法观察到的:

       -- 1.                    -- 2.
     a a a a b b b            a a a a b b b
           a     b            a       b

但如果有一个中间人物,那么我们当然可以看到区别:

       -- 1.                    -- 2.
     a a a a b b b a          a a a a b b b a
                 b a          a       b

【讨论】:

    猜你喜欢
    • 2016-08-22
    • 2021-11-19
    • 2017-11-26
    • 1970-01-01
    • 1970-01-01
    • 2018-07-15
    • 1970-01-01
    • 2016-11-04
    • 2020-01-05
    相关资源
    最近更新 更多