【问题标题】:How to implement delete with foldr in Haskell如何在 Haskell 中使用 foldr 实现删除
【发布时间】:2014-12-13 06:46:22
【问题描述】:

过去几天我一直在研究褶皱。我可以用它们实现简单的功能,例如lengthconcatfilter。我坚持的是尝试使用 deletetakefindfoldr 函数来实现。我已经通过显式递归实现了这些,但对我来说如何将这些类型的函数转换为正确的折叠似乎并不明显。

我学习了 Graham Hutton 和 Bernie Pope 的教程。模仿 Hutton 的dropWhile,我能够用foldr 实现delete,但它在无限列表上失败。

从阅读Implement insert in haskell with foldrHow can this function be written using foldr?Implementing take using foldr 来看,我似乎需要使用foldr 来生成一个函数,然后该函数会执行某些操作。但我并不真正了解这些解决方案,也不知道如何以这种方式实施例如delete

您能否向我解释一下使用 foldr 延迟版本的函数(如我提到的那些)实现的一般策略。也许您也可以实现delete 作为示例,因为这可能是最简单的方法之一。

我正在寻找一个初学者可以理解的详细解释。我对解决方案不感兴趣,我想加深理解,这样我就可以自己想出类似问题的解决方案。

谢谢。

编辑: 在撰写本文时,有一个有用的答案,但这并不是我想要的。我对使用 foldr 生成函数的方法更感兴趣,然后它会做一些事情。我的问题中的链接有这方面的例子。我不太了解这些解决方案,因此我想了解有关此方法的更多信息。

【问题讨论】:

    标签: haskell fold


    【解决方案1】:

    delete 是一种模态搜索。它有两种不同的操作模式——无论是否已经找到结果。您可以使用foldr 构造一个函数,在检查每个元素时将状态向下传递。所以在delete的情况下,状态可以是一个简单的Bool。这不是最好的类型,但可以。

    一旦您确定了状态类型,您就可以开始处理foldr 构造。我将按照我的方式逐步解决。我将启用ScopedTypeVariables,以便更好地注释子表达式的类型。如果您知道状态类型,就知道您希望foldr 生成一个函数,该函数采用该类型的值,并返回所需的最终类型的值。这足以开始绘制草图了。

    {-# LANGUAGE ScopedTypeVariables #-}
    
    delete :: forall a. Eq a => a -> [a] -> [a]
    delete a xs = foldr f undefined xs undefined
      where
        f :: a -> (Bool -> [a]) -> (Bool -> [a])
        f x g = undefined
    

    这是一个开始。 g 的确切含义在这里有点棘手。它实际上是处理列表其余部分的函数。事实上,将其视为延续是准确的。它绝对代表以您选择传递的任何状态执行其余的折叠。鉴于此,是时候弄清楚在其中一些 undefined 位置放置什么了。

    {-# LANGUAGE ScopedTypeVariables #-}
    
    delete :: forall a. Eq a => a -> [a] -> [a]
    delete a xs = foldr f undefined xs undefined
      where
        f :: a -> (Bool -> [a]) -> (Bool -> [a])
        f x g found | x == a && not found = g True
                    | otherwise           = x : g found
    

    这似乎相对简单。如果当前元素是正在搜索的元素,但尚未找到,则不输出,继续状态设置为True,表示已找到。 otherwise,输出当前值,继续当前状态。这只是将其余参数留给foldr。最后一个是初始状态。另一个是空列表的状态函数。好的,这些也不错。

    {-# LANGUAGE ScopedTypeVariables #-}
    
    delete :: forall a. Eq a => a -> [a] -> [a]
    delete a xs = foldr f (const []) xs False
      where
        f :: a -> (Bool -> [a]) -> (Bool -> [a])
        f x g found | x == a && not found = g True
                    | otherwise           = x : g found
    

    无论状态如何,遇到空列表时产生一个空列表。并且初始状态是尚未找到要搜索的元素。

    这种技术也适用于其他情况。例如,foldl 可以这样写成foldr。如果您将foldl 视为重复转换初始累加器的函数,您可以猜到这就是正在生成的函数 - 如何转换初始值。

    {-# LANGUAGE ScopedTypeVariables #-}
    
    foldl :: forall a b. (a -> b -> a) -> a -> [b] -> a
    foldl f z xs = foldr g id xs z
      where
        g :: b -> (a -> a) -> (a -> a)
        g x cont acc = undefined
    

    当问题被定义为操纵初始累加器时,基本情况并不难找到,在那里命名为z。空列表为恒等变换id,传递给创建函数的值为z

    g 的实现比较复杂。它不能只是盲目地对类型进行,因为有两种不同的实现使用所有预期值和类型检查。这是类型不够用的情况,需要考虑可用函数的含义。

    让我们从看起来应该使用的值及其类型的清单开始。 g 的正文中似乎必须使用的东西是f :: a -> b -> ax :: bcont :: (a -> a)acc :: af 显然会将x 作为它的第二个参数,但是有一个问题是在适当的地方使用cont。要弄清楚它的去向,请记住它表示处理列表的其余部分返回的转换函数,foldl 处理当前元素,然后将该处理的结果传递给列表的其余部分。

    {-# LANGUAGE ScopedTypeVariables #-}
    
    foldl :: forall a b. (a -> b -> a) -> a -> [b] -> a
    foldl f z xs = foldr g id xs z
      where
        g :: b -> (a -> a) -> (a -> a)
        g x cont acc = cont $ f acc x
    

    这也表明foldl' 可以这样写,只需稍作改动:

    {-# LANGUAGE ScopedTypeVariables #-}
    
    foldl' :: forall a b. (a -> b -> a) -> a -> [b] -> a
    foldl' f z xs = foldr g id xs z
      where
        g :: b -> (a -> a) -> (a -> a)
        g x cont acc = cont $! f acc x
    

    不同之处在于($!) 用于建议在将f acc x 传递给cont 之前对其进行评估。 (我说“建议”是因为在某些极端情况下,($!) 甚至不会强制评估 WHNF。)

    【讨论】:

      【解决方案2】:

      delete 不会均匀地对整个列表进行操作。计算的结构不仅仅是一次考虑整个列表一个元素。在它击中它正在寻找的元素后它会有所不同。这告诉您它不能作为只是 foldr 来实现。必须进行某种后处理。

      发生这种情况时,一般模式是您构建一对值,并在完成foldr 时取其中一个。这可能是您在模仿 Hutton 的dropWhile 时所做的,尽管我不确定,因为您没有包含代码。像这样?

      delete :: Eq a => a -> [a] -> [a]
      delete a = snd . foldr (\x (xs1, xs2) -> if x == a then (x:xs1, xs1) else (x:xs1, x:xs2)) ([], [])
      

      主要思想是xs1 始终是列表的完整尾部,而xs2delete 在列表尾部上方的结果。由于您只想删除匹配的第一个元素,因此当您匹配要搜索的值时,您不想在尾部使用 delete 的结果,您只想返回列表的其余部分不变 - 幸运的是,xs1 中始终存在的内容。

      是的,这不适用于无限列表 - 但仅出于一个非常具体的原因。 lambda 太严格了。 foldr 仅适用于无限列表时,它提供的函数并不总是强制评估其第二个参数,并且 lambda 总是强制评估其模式匹配中的第二个参数对。切换到无可辩驳的模式匹配可以解决这个问题,方法是允许 lambda 在检查其第二个参数之前生成构造函数。

      delete :: Eq a => a -> [a] -> [a]
      delete a = snd . foldr (\x ~(xs1, xs2) -> if x == a then (x:xs1, xs1) else (x:xs1, x:xs2)) ([], [])
      

      这不是获得该结果的唯一方法。使用 let-binding 或 fstsnd 作为元组上的访问器也可以完成这项工作。但这是差异最小的变化。

      这里最重要的一点是在处理传递给foldr 的归约函数的第二个参数时要非常小心。您希望尽可能推迟检查第二个参数,以便foldr 可以在尽可能多的情况下延迟传输。

      如果您查看该 lambda,您会发现在对归约函数的第二个参数执行任何操作之前选择了所采用的分支。此外,您会看到大多数情况下,归约函数在需要计算第二个参数之前,会在结果元组的两半中生成一个列表构造函数。由于这些列表构造函数是 delete 的组成部分,因此它们对于流式传输很重要 - 只要您不让这对构成障碍。使这对上的模式匹配无可辩驳是让它不碍事的原因。

      作为foldr 的流媒体属性的额外示例,请考虑我最喜欢的示例:

      dropWhileEnd :: (a -> Bool) -> [a] -> [a]
      dropWhileEnd p = foldr (\x xs -> if p x && null xs then [] else x:xs) []
      

      它可以流式传输 - 尽可能多。如果您确切地弄清楚它何时以及为什么会进行流式传输,您将几乎了解foldr 的流式传输结构的每一个细节。

      【讨论】:

      • 感谢您的回答并觉得它很有用。如果我有足够的声誉,我会投票,但我还不接受,因为你的答案不是我想要的。但这是我没有让问题更精确的错,我会看看我是否可以编辑。我对使用 foldr 生成函数的方法更感兴趣。你是对的,你的第一个实现几乎就是我尝试过的。
      • @user168064 好的。在花了大约 3 小时学习该主题后,我也可以回答您的预期问题。我认为它应该是第二个答案,而不是对这个答案的编辑,所以另一个答案即将到来。
      【解决方案3】:

      这是一个简单的删除,用 foldr 实现:

      delete :: (Eq a) => a -> [a] -> [a]
      delete a xs = foldr (\x xs -> if x == a then (xs) else (x:xs)) [] xs
      

      【讨论】:

      • 这不是删除。删除仅删除第一次出现。这就是“挑战”所在。
      猜你喜欢
      • 2016-09-04
      • 2014-01-01
      • 1970-01-01
      • 1970-01-01
      • 2021-04-15
      • 1970-01-01
      • 1970-01-01
      • 2010-09-19
      • 2013-03-30
      相关资源
      最近更新 更多