【问题标题】:Haskell 99 questions #8: Can't understand foldrHaskell 99 题 #8:看不懂 foldr
【发布时间】:2015-01-23 09:25:38
【问题描述】:

我正在练习 Haskell 99 questions

问题 8

消除列表元素的连续重复。

如果列表包含重复的元素,则应将其替换为 元素的单个副本。元素的顺序不应该是 改变了。

Haskell 中的示例:

 > compress "aaaabccaadeeee" 

 "abcade"

我无法理解这个解决方案:

compress xs = foldr f (const []) xs Nothing
  where
    f x r a@(Just q) | x == q = r a
    f x r _ = x : r (Just x)

foldr 接受三个参数。第一个参数是函数(a -> b -> b)。第二个是初始累加器,第三个是列表。

(const []) 是传递给foldr 的第二个参数吗?

函数f :: Eq a => a -> (Maybe a -> [a]) -> Maybe a -> [a] 采用三个参数,与foldr 的预期不符。传入了什么值?

最后一个Nothing是干什么用的?

【问题讨论】:

  • 我也很难理解这个问题。我的猜测是 foldr 正在构建并返回一个函数,然后 Nothing 是该函数的输入......
  • @MathematicalOrchid 听起来不错。 f 的类型签名帮助:f :: Eq a => a -> (Maybe a -> [a]) -> (Maybe a -> [a])

标签: haskell


【解决方案1】:

你说

foldr 接受三个参数

这是误导。 foldr的类型是

(a -> r -> r) -> r -> [a] -> r

其中r 是一个类型变量,可以被任何类型实例化,包括函数类型。这就是这里发生的事情,并且确实给出了提示,传递给foldr(应该匹配r)的第二个参数是const []

这里使用foldr的类型如下:

foldr :: Eq a => (a -> (Maybe a -> [a]) -> Maybe a -> [a]) -> (Maybe a -> [a]) -> [a] -> Maybe a -> [a]
foldr ::         (a -> r                -> r             ) -> r                -> [a] -> r

一旦您知道r 被实例化为Maybe a -> [a],很明显f 在这里有效地接受三个参数,而foldr 接受四个参数。

同样现象的一个更简单的例子是这样的:

id id 2

在这里,id 似乎有两个参数。外部id 用于函数类型,内部idInteger 参数一起使用。

【讨论】:

    【解决方案2】:

    正如 Jubobs 指出的,f 具有签名f :: Eq a => a -> (Maybe a -> [a]) -> (Maybe a -> [a])foldr 的签名上下文中,这意味着aa,而b(Maybe a -> [a])。所以foldr确实构建了一个复杂的函数。

    我们来看"aab"这个列表,一个很简单的输入。折叠展开为

    (f 'a' (f 'a' (f 'b' (const []))))
    

    Nothing 传递给该构造,您可以开始扩展它。然后最外面的调用有

    x = 'a'
    r = <the inner stuff>
    a@(Just q) fails to match Nothing
    _ matches Nothing
    

    所以它的结果是'a' : r (Just 'a')。替换 r 我们得到:

    'a' : f 'a' (f 'b' (const [])) (Just 'a')
    

    f 的新最外层调用有

    x = 'a'
    r = (f 'b' (const []))
    a@(Just q) matches Just 'a'
    guard 'a' == 'a' is true
    

    所以r a 分支被采用,即它只是转发到链中的下一个。函数的结果是r (Just 'a'),当前整个表达式的结果是'a' : r (Just 'a')。再次替换r

    'a' : f 'b' (const []) (Just 'a')
    

    f的新参数:

    x = 'b'
    r = const []
    a@(Just q) matches Just 'a'
    guard 'b' == 'a' is false
    _ matches Just 'a'
    

    我们再次选择第二个选项,因为守卫失败了。守卫基本上说,“如果前一个字符(qaNothing,如果没有前一个字符)与当前字符相同(x),不要把它放在输出中,否则做”。

    所以函数结果是'b' : const [] (Just 'b'),而我们目前看到的完整表达式结果是'a' : 'b' : const [] (Just 'b')

    const 函数忽略它的第二个参数并返回第一个参数。它看起来像这样:

    const :: a -> b -> a
    const x _ = x
    

    在我们表达式的调用中,参数匹配如下:

    x = []
    _ = Just 'b'
    

    因此结果是[]。整个表达式现在是'a' : 'b' : [],或者在字符串语法中,"ab"。重复的'a' 已被删除。

    如果你愿意,你可以在纸上玩这个游戏以获得更长的表达。

    该解决方案基本上使用foldr 构建一个使用延续传递样式的函数,以便它可以携带状态。这是一种时髦的技术。通过折叠携带状态的一种更直观的方法是使折叠结果成为实际累加器和您想要携带的状态的元组,然后在最后提取正确的结果。对于您的问题,可能如下所示:

    compress s = fst $ foldr f ([], Nothing) s
      where
        f cur (acc, (Just prev)) | cur == prev = (acc, (Just prev))
        f cur (acc, _) = (cur : acc, Just cur)
    

    这应该也可以。但是请注意,这反转状态的顺序,即它向后遍历字符串。我不确定一个与另一个的性能或懒惰是什么。我觉得CPS方案比较懒,但是我不确定。

    【讨论】:

    • 解释得很清楚。在最后一个直观的解决方案中,应该是compress s = fst $ foldr f ([], Nothing) s
    • @aleung 是的,应该。已编辑。
    猜你喜欢
    • 2019-05-26
    • 2016-02-15
    • 1970-01-01
    • 1970-01-01
    • 2015-01-08
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-01-30
    相关资源
    最近更新 更多