【问题标题】:How are Haskell guards evaluated?Haskell 守卫是如何评估的?
【发布时间】:2014-12-22 19:27:09
【问题描述】:

我正在做 99 个 Haskell 问题,在其中一个解决方案中我遇到了以下代码:

pack' [] = []
pack' [x] = [[x]]
pack' (x:xs)
    | x == head  h_p_xs = (x:h_p_xs):t_p_hs
    | otherwise         = [x]:p_xs
    where p_xs@(h_p_xs:t_p_hs) = pack' xs

我想知道 pack' 何时在第一个守卫中被调用,这是否是 Haskell 代码中引用从函数调用返回的列表的头部和尾部的常见模式。在任何级别的递归中是否有多个调用 pack',这是一个快速的解决方案吗?

【问题讨论】:

    标签: haskell


    【解决方案1】:

    我想知道第一个守卫何时调用 pack'

    守卫x == head h_p_xs 强制评估h_p_xs,这会触发递归调用。

    如果这是 Haskell 代码中的一种常见模式,用于引用 从函数调用返回的列表。

    我认为这是一种很常见的模式。您还可以使用 case pack' xs of ...let ... = pack' xs in ... 来找到变体。

    请注意,将letwhereh_p_xs:t_p_xs 之类的模式一起使用将在找到空列表时导致运行时错误。此代码小心确保递归调用不会返回 emlty 列表。

    是否有多个调用打包' 在任何级别的递归

    为了迂腐,Haskell 标准没有具体说明代码是如何被实际评估的,而只是说明结果是什么。因此,理论上,编译器可以进行任意数量的递归调用。

    实际上,编译器会小心地只进行一次递归调用——不这样做会导致糟糕的性能。

    为了比较,下面的代码是等价的,但会导致指数复杂度(!)

    ...
    where p_xs = h_p_xs:t_p_hs
          h_p_xs = head (pack' xs)
          t_p_xs = tail (pack' xs)
    

    在这里,您可以期望编译器进行两次递归调用。

    这是一个快速的解决方案吗?

    是的。它预计在输入上以线性时间运行。

    【讨论】:

    • 如果我将 x == head h_p_xs = (x:h_p_xs):t_p_hs 更改为 x == head xs = (x:h_p_xs):t_p_hs 程序运行方式有什么不同吗?
    • @ÞórÓðinsson 据我现在所见,它应该是等价的。
    【解决方案2】:

    pack' 似乎像 Data.List.group 一样工作 - 它将连续相等的元素组合到一个列表中。所以,pack' [1,1,3,2,2] 应该返回 [[1,1], [3], [2,2]]。

    模式匹配是惯用的haskell。这里,在声明中,

    h_p_xs:t_p_hs = pack' hs
    

    我们知道 pack' hs 返回一个列表。因此,h_p_xs 匹配到它的头部,而 t_p_hs 匹配到它的尾部。 LHS 和 RHS 都必须具有与此处相同的结构(此处双方都有列表结构),以便在 Haskell 中进行模式匹配。 p_xs 在此处与整个 RHS 匹配(@模式)。所以,是的,这是 Haskell 中非常常见的习语。

    在包的定义中,前两行负责空列表和单例列表。因此,第一个保护条件仅适用于输入列表具有多个元素且其第一个和第二个元素相等时。

    在任何级别,最多有一次对 pack' 的递归调用,因此时间复杂度在列表长度上为 O(n)。应该很快。

    【讨论】:

    • 其实这不是尾递归。不过,正如您所说,复杂性是 O(n)。
    【解决方案3】:

    这是引用 head 的常用方法,如果它使代码更具可读性。当然,你可以通过在最后一行做类似

    p_xs@(  (h_p_x : h_p_xs)  : t_p_hs) = pack' xs
    

    而不是

    p_xs@( h_p_xs : t_p_hs ) = pack' xs
    

    这样您就可以在h_p_x 变量中找到头部。但是你需要在第四行添加:

    | x == head  h_p_xs = (x : h_p_x: h_p_xs):t_p_hs
    

    所以你看,在这里使用: 操作符只是通过添加无用的实体来弄乱代码。 至于递归的次数,我这里每一层只能看到一个递归调用,所以基本上是线性运行时间,因此有效。

    【讨论】:

      猜你喜欢
      • 2021-04-04
      • 1970-01-01
      • 2020-05-03
      • 2019-03-01
      • 1970-01-01
      • 1970-01-01
      • 2018-04-03
      • 2017-07-26
      • 1970-01-01
      相关资源
      最近更新 更多