【问题标题】:Intermediate value in simple Haskell function简单 Haskell 函数中的中间值
【发布时间】:2019-02-20 15:08:55
【问题描述】:

我需要一个函数将列表中的每个其他数字加倍。这就是诀窍:

doubleEveryOther :: [Integer] -> [Integer]
doubleEveryOther []         = []
doubleEveryOther (x:[])     = [x]
doubleEveryOther (x:(y:zs)) = x : 2 * y : doubleEveryOther zs

但是,问题是我需要从 从右边开始每隔一个数字加倍 - 所以如果列表的长度是偶数,第一个将加倍,等等。

我知道在 Haskell 中反向操作列表很棘手,所以我的计划是反转列表,应用我的函数,然后再次输出反向。我有一个reverseList 函数:

reverseList :: [Integer] -> [Integer]
reverseList  [] = []
reverseList  xs = last xs : reverseList (init xs) 

但我不太确定如何将它植入我的原始函数中。我遇到了这样的事情:

doubleEveryOther :: [Integer] -> [Integer]
doubleEveryOther []         = []
doubleEveryOther (x:[])     = [x]
doubleEveryOther (x:(y:zs)) =
 | rev_list = reverseList (x:(y:zs))
 |  rev_list = [2 * x, y] ++ doubleEveryOther zs

我不太确定包含此类中间值的函数的语法。

如果相关,这是CIS 194 HW 1 中的练习 2。

【问题讨论】:

  • 提示:保留旧的doubleEveryOther,写一个新的函数,新的名字结合reverseListdoubleEveryOther

标签: haskell


【解决方案1】:

这是您已经创建的两个函数的非常简单的组合:

doubleEveryOtherFromRight = reverseList . doubleEveryOther . reverseList

请注意,您的 reverseList 实际上已经在标准 Prelude 中定义为 reverse。所以你不需要自己定义它。

我知道上述解决方案效率不高,因为reverse 的两种用法都需要遍历整个列表。我将把它留给其他人来建议更有效的版本,但希望这能说明函数组合的强大功能,可以从更简单的计算中构建更复杂的计算。

【讨论】:

  • 正是我想要的,非常感谢!我确信有更有效的方法,但我只需要克服这个障碍。
【解决方案2】:

正如 Lorenzo 所指出的,您可以通过一次来确定列表的长度是奇数还是偶数,然后再通过第二次来实际构建新列表。不过,将这两个任务分开可能会更简单。

doubleFromRight ls = zipWith ($) (cycle fs) ls -- [f0 ls0, f1 ls1, f2 ls2, ...]
   where fs = if odd (length ls)
              then [(*2), id]
              else [id, (*2)]

那么这是如何工作的呢?首先,我们观察到要创建最终结果,我们需要将两个函数之一(id(*2))应用于ls 的每个元素。 zipWith 可以做到这一点,如果我们有一个适当的功能列表。其定义中有趣的部分基本上是

zipWith f (x:xs) (y:ys) = f x y : zipWith f xs ys

f($) 时,我们只是将一个列表中的函数应用到另一个列表中的相应元素。

我们想压缩lsid(*2) 的无限交替列表。问题是,该列表应该从哪个函数开始?它应该始终以(*2)结束,因此起始项由ls 的长度决定。奇数长度要求我们以(*2) 开头;偶数,id

【讨论】:

  • 有什么理由使用 if-then-else 代替守卫?
【解决方案3】:

大多数其他解决方案向您展示了如何使用已有的构建块或标准库中可用的构建块来构建您的函数。我认为看看你如何从头开始构建它也很有启发性,所以在这个答案中我讨论了一个想法。

计划如下:我们要一直走到列表的末尾,然后再回到前面。我们将在从最后走回来的过程中建立我们的新清单。当我们往回走时,我们将构建它的方式是在(乘法)因子 1 和 2 之间交替,将当前元素乘以当前因子,然后交换因子以进行下一步。最后,我们将返回最终因子和新列表。所以:

doubleFromRight_ :: Num a => [a] -> (a, [a])
doubleFromRight_ [] = (1, [])
doubleFromRight_ (x:xs) =
    -- not at the end yet, keep walking
    let (factor, xs') = doubleFromRight_ xs
    -- on our way back to the front now
    in (3-factor, factor*x:xs')

如果你愿意,你可以写一个小包装器,最后扔掉这个因素。

doubleFromRight :: Num a => [a] -> [a]
doubleFromRight = snd . doubleFromRight_

在 ghci 中:

> doubleFromRight [1..5]
[1,4,3,8,5]
> doubleFromRight [1..6]
[2,2,6,4,10,6]

现代做法是将帮助函数 doubleFromRight_ 隐藏在 doubleFromRight 中的 where 块内;由于稍微修改的名称实际上并没有告诉您任何新内容,因此我们将在内部使用社区标准名称。这两个变化可能会让你来到这里:

doubleFromRight :: Num a => [a] -> [a]
doubleFromRight = snd . go where
    go [] = (1, [])
    go (x:xs) = let (factor, xs') = go xs in (3-factor, factor*x:xs')

然后,高级 Haskeller 可能会注意到 go 适合折叠的形状并写下:

doubleFromRight :: Num a => [a] -> [a]
doubleFromRight = snd . foldr (\x (factor, xs) -> (3-factor, factor*x:xs)) (1,[])

但我认为在这种情况下,使用显式递归提前一步停止是完全可以的;在这种情况下,它甚至可能更具可读性!

【讨论】:

  • 感谢丹尼尔的回答。为这个模糊的问题道歉,但 (3-factor, factor*x:xs') 到底做了什么?
  • @JoshFriedlander 3-factorfactor*x 分别只是减法和乘法; : 是将元素添加到列表的函数; (,) 是对的构造函数(如果你喜欢这个名字,也就是元组)。
  • 那么3 - factor 是否让我们在列表中后退三步?
  • @JoshFriedlander 不。在 1 和 2 之间交替只是一个技巧:3-1 = 23-2 = 1。所以如果当前因子是1,那么3-factor就是2的新因子;如果当前因子是2,那么3-factor就是1的新因子。
  • 啊,现在我明白了。虽然@Robin Zigmond 的答案是我一直在寻找的,但慢慢阅读您的答案/要求澄清让我学到了很多新东西。感谢您的宝贵时间!
【解决方案4】:

当您必须处理列表中的连续术语时,zip 与列表理解是一种简单的方法。它需要两个列表并返回一个元组列表,因此您可以使用其尾部压缩列表或将其编入索引。我的意思是

doubleFromRight :: [Int] -> [Int]
doubleFromRight ls = [if (odd i == oddness) then 2*x else x | (i,x) <- zip [1..] ls]
    where
      oddness = odd . length $ ls

这样你计算每个元素,从 1 开始,如果索引与列表中的最后一个元素具有相同的奇偶性(奇数或偶数),则将元素加倍,否则保持原样。

我不是 100% 肯定这会更有效,但如果有人能在 cmets 中指出它会很棒

【讨论】:

  • 更好的是,使用zipWith ($) listOfFuncs ls,其中listOfFuncscycle [id, (*2)]cycle [(*2), id],具体取决于列表是否为奇数。
  • @chepner,这是一个非常简洁、易于理解且(据我所知)有效的解决方案,您应该自己将其发布为答案
【解决方案5】:

如果我们真的想避免计算长度,我们可以定义

doubleFromRight :: Num a => [a] -> [a]
doubleFromRight xs = zipWith ($)
                       (foldl' (\a _ -> drop 1 a) (cycle [(2*), id]) xs)
                       xs

这将输入列表与循环的无限函数列表配对,[(*2), id, (*2), id, .... ]。然后它会跳过它们。当第一个列表完成时,第二个处于适当的状态,可以 - 再次 - 成对应用, - 在第二个!这一次,是真的。

所以实际上它确实测量长度(当然),它只是不计入整数,而是计入列表元素。

如果列表的长度是偶数,则第一个元素将加倍,否则第二个,正如您在问题中指定的那样:

> doubleFromRight [1..4]
[2,2,6,4]

> doubleFromRight [1..5]
[1,4,3,8,5]

foldl' 函数从左到右处理列表。它的类型是

foldl' :: (b -> a -> b) -> b -> [a] -> b
--        reducer_func    acc   xs    result

【讨论】:

  • 如果这个算法不计算长度,你能澄清一下这个算法是如何决定从哪个开始的吗?
  • 它将输入列表与循环的 infinite 函数列表配对,[(*2), id, (*2), id, .... ]。然后它会跳过它们。当第一个列表完成时,第二个处于适当的状态,可以 - 再次 - 成对应用 - 在第二个!
  • 感谢您的参与。在我的代码中很难说清楚或不明确。 :)
  • 感谢您的帮助! Haskell 社区(这里和 IRC 上)非常有耐心,对新手很有帮助。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-05-27
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多