【问题标题】:How would you define map and filter using foldr in Haskell?您将如何在 Haskell 中使用 foldr 定义映射和过滤器?
【发布时间】:2011-08-09 05:54:37
【问题描述】:

我正在对函数式语言(目前使用 Haskell)进行一些自学。我遇到了一个基于 Haskell 的作业,它需要根据 foldr 定义映射和过滤器。在我的一生中,我并没有完全理解如何去做。

例如,当我定义一个地图函数时:

map'            :: (a -> b) -> [a] -> [b]
map' f []       = []
map' f (x:xs)   = foldr (\x xs -> (f x):xs) [] xs

我不知道为什么列表的第一个元素总是被忽略。意思是:

map' (*2) [1,2,3,4]

导致 [4,6,8] 而不是 [2,4,6,8]

同样,我的过滤器功能:

filter'             :: (a -> Bool) -> [a] -> [a]
filter' p []        = []
filter' p (x:xs)    = foldr (\x xs -> if p x then x:xs else xs ) [] xs

当运行时:

filter' even [2,3,4,5,6]

导致 [4,6] 而不是 [2,4,6]

为什么会这样?我应该如何定义这些函数以获得预期的结果?我假设我的 lambda 表达式有问题...

【问题讨论】:

    标签: haskell map functional-programming filter fold


    【解决方案1】:

    在您的定义中,您正在为 x:xs 进行模式匹配,这意味着,当您的参数为 [1,2,3,4] 时,x 绑定到 1 并且 xs 绑定到列表的其余部分: [2,3,4].

    你不应该做的只是扔掉x: 部分。然后你的foldr 将在整个列表中工作。

    所以你的定义应该如下所示:

    map'            :: (a -> b) -> [a] -> [b]
    map' f []       = []
    map' f xs       = foldr (\x xs -> (f x):xs) [] xs
    

    filter'             :: (a -> Bool) -> [a] -> [a]
    filter' p []        = []
    filter' p xs        = foldr (\x xs -> if p x then x:xs else xs ) [] xs
    

    【讨论】:

    • [] 的情况是多余的 --- foldr 当你传递它时会做正确的事情[]
    • 是的。但亚历山德罗已经写好了。因此,要进一步改进,请查看他的答案。
    【解决方案2】:

    对于您的第一个问题,foldr 已经为空列表提供了案例,因此您不需要也不应该在自己的地图中提供案例。

    map' f = foldr (\x xs -> f x : xs) []
    

    filter' 也是如此

    filter' p = foldr (\x xs -> if p x then x : xs else xs) []
    

    您的 lambda 表达式没有问题,但您对 filter'map' 的定义有问题。在 cons 情况下 (x:xs),你吃掉头部 (x),然后将尾部传递给 foldrfoldr 函数永远看不到你已经吃过的第一个元素。 :)

    另请注意:

    filter' p = foldr (\x xs -> if p x then x : xs else xs) []
    

    等同于 (η-equivalent):

    filter' p xs = foldr (\x xs -> if p x then x : xs else xs) [] xs
    

    【讨论】:

    • 感谢您让我知道为空列表添加案例有点多余。
    【解决方案3】:

    您的解决方案几乎可以正常工作。) 问题是您在两个函数中都有两个不同的 x 绑定(在模式匹配内部和 lambda 表达式内部),因此您失去了对第一个元素的跟踪。

    map'            :: (a -> b) -> [a] -> [b]
    map' f []       = []
    map' f (x:xs)   = foldr (\x xs -> (f x):xs) [] (x:xs)
    
    filter'             :: (a -> Bool) -> [a] -> [a]
    filter' p []        = []
    filter' p (x:xs)    = foldr (\x xs -> if p x then x:xs else xs ) [] (x:xs)
    

    这应该是诀窍:)。另外:您可以轻松编写无点风格的函数。

    【讨论】:

      【解决方案4】:

      我希望我可以发表评论,但是,我没有足够的业力。

      其他答案都很好,但我认为最大的困惑似乎源于您对 x 和 xs 的使用。

      如果你把它改写成

      map'            :: (a -> b) -> [a] -> [b]
      map' f []       = []
      map' f (x:xs)   = foldr (\y ys -> (f y):ys) [] xs
      

      您会清楚地看到x 甚至没有在右侧提及,因此它不可能出现在解决方案中。

      干杯

      【讨论】:

      • 没错。因此应该很清楚,map' f xs 而不是map' f (x:xs) 应该可以解决问题。
      • Btw GHC 在使用选项-Wall 时可以帮助查看它:Warning: Defined but not used: 'x'(或者,对于原始代码:This binding for 'x' shadows the existing binding
      • 现在,你应该有足够的业力和一个漂亮的徽章;)
      • 谢谢,你是对的......我对 x 和 xs 的使用让我感到困惑!这确实使它更加清晰。
      • 请按照 Dan Burton 的建议将 map' f (x:xs) 替换为 map' f xs
      【解决方案5】:

      我会使用 foldr 和 function composition 定义 map 如下:

      map :: (a -> b) -> [a] -> [b]
      map f = foldr ((:).f) []
      

      对于过滤器的情况:

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

      请注意,使用 foldr 或 foldl 在列表上定义函数时,不必传递列表本身。 您的解决方案的问题是您放下列表的头部,然后将地图应用于列表并 这就是为什么显示结果时缺少列表头部的原因。

      【讨论】:

      • 这回答了标题中的一个问题,非常简洁,但是如果你解释一下会更有帮助,如果你回答了问题中的其他问题会更有帮助。
      • 按照@coffeMug ​​的建议,也可以在 lambda 中减少参数:filter p = foldr (\x -> if p x then (x:) else id) []
      • 真的有人觉得(:).f\x xs -> f x : xs 更容易阅读吗?
      • @paluh,虽然这很愚蠢,但您可以导入 Data.Bool 并使用 filter p = foldr (bool id . (:) <*> p) []。走极端,filter = (`foldr` []) . (bool id . (:) <*>)
      • @Justin L 是一位数学家。 ;-)
      【解决方案6】:

      我是 Haskell 的新手(事实上我发现这个页面问了同样的问题)但这是我迄今为止对列表和文件夹的理解:

      • 列表是使用 cons (:) 运算符链接到下一个元素的元素。它们以空列表[] 终止。 (将其视为二元运算符,就像加法一样 (+) 1+2+3+4 = 10, 1:2:3:4:[] = [1,2,3,4]
      • foldr 函数采用一个带有两个参数的函数。这将替换 cons 运算符,后者将定义每个项目如何链接到下一个项目。
      • 它还采用操作的终端值,可以将其作为将分配给空列表的初始值。对于缺点,它是空列表[]。如果将空列表链接到任何列表,则结果就是列表本身。所以对于sumfunction,它是0。对于乘法函数,它是1,等等。
      • 它需要列表本身

      所以我的解决方案如下:

      filter' p = foldr (\x n -> if p x then x : n else n) []
      

      lambda 表达式是我们的链接函数,它将被用来代替 cons (:) 运算符。空列表是我们对空列表的默认值。如果满足谓词,我们照常使用(:) 链接到下一个项目,否则我们根本不链接。

      map' f = foldr (\x n -> f x : n) []
      

      在这里,我们将f x 链接到下一个项目,而不仅仅是x,这只会复制列表。

      另外,请注意您不需要使用模式匹配,因为我们已经告诉 foldr 在出现空列表时该怎么做。

      我知道这个问题很老了,但我还是想回答一下。我希望它不违反规则。

      【讨论】:

        【解决方案7】:

        另一种思考方式 - foldr 之所以存在,是因为经常使用以下递归模式:

        -- Example 1: Sum up numbers
        summa :: Num a => [a] -> a
        summa []     = 0
        summa (x:xs) = x + suma xs
        

        取数字的乘积甚至反转一个列表在结构上看起来与前面的递归函数非常相似:

        -- Example 2: Reverse numbers
        reverso :: [a] -> [a]
        reverso []      = []
        reverso (x:xs)  = x `op` reverso xs
          where
            op = (\curr acc -> acc ++ [curr])
        

        上述示例中的结构仅在初始值(0 用于 summa 和 [] 用于反向)以及第一个值和递归调用之间的运算符(+ 用于 summa 和 (\q qs -> qs ++ [q])反向)。所以上面例子的函数结构一般可以看成是

        -- Generic function structure
        foo :: (a -> [a] -> [a]) -> [a] -> [a] -> [a]
        foo op init_val []      = init_val
        foo op init_val (x:xs)  = x `op` foo op init_val xs
        

        要查看这个“通用” foo 是否有效,我们现在可以使用 foo 重写逆向运算,并将运算符、初始值和列表本身传递给它:

        -- Test: reverso using foo
        foo (\curr acc -> acc ++ [curr]) [] [1,2,3,4]
        

        让我们给 foo 一个更通用的类型签名,以便它也适用于其他问题:

        foo :: (a -> b -> b) -> b -> [a] -> b
        

        现在,回到您的问题 - 我们可以这样编写过滤器:

        -- Example 3: filter
        filtero :: (a -> Bool) -> [a] -> [a]
        filtero p []     = []
        filtero p (x:xs) = x `filterLogic` (filtero p xs)
          where
             filterLogic = (\curr acc -> if (p curr) then curr:acc else acc)
        

        这又与 summa 和 reverso 具有非常相似的结构。因此,我们应该能够使用 foo 来重写它。假设我们要过滤列表 [1,2,3,4] 中的偶数。然后我们再次传递 foo 运算符(在本例中为 filterLogic)、初始值和列表本身。在此示例中,filterLogic 采用 p 函数,称为谓词,我们必须为调用定义它:

        let p = even in foo (\curr acc -> if (p curr) then curr:acc else acc) [] [1,2,3,4] 
        

        在 Haskell 中,foo 被称为 foldr。因此,我们使用 foldr 重写了过滤器。

        let p = even in foldr (\curr acc -> if (p curr) then curr:acc else acc) [] [1,2,3,4] 
        

        如我们所见,filter 可以用 foldr 编写:

        -- Solution 1: filter using foldr
        filtero' :: (a -> Bool) -> [a] -> [a]
        filtero' p xs = foldr (\curr acc -> if (p curr) then curr:acc else acc) [] xs 
        

        至于ma​​p,我们也可以写成

        -- Example 4: map
        mapo :: (a -> b) -> [a] -> [b]
        mapo f []   = []
        mapo f (x:xs) = x `op` (mapo f xs)
          where
            op = (\curr acc -> (f curr) : acc)
        

        因此可以使用 foldr 重写。例如,将列表中的每个数字都乘以 2:

        let f = (* 2) in foldr (\curr acc -> (f curr) : acc) [] [1,2,3,4]
        

        所以,正如我们所见,ma​​p 可以用 foldr 编写:

        -- Solution 2: map using foldr
        mapo' :: (a -> b) -> [a] -> [b]
        mapo' f xs = foldr (\curr acc -> (f curr) : acc) [] xs
        

        【讨论】:

          【解决方案8】:
          *Main> :{
          *Main| map' :: (a -> b) -> [a] -> [b]
          *Main| map' = \f -> \ys -> (foldr (\x -> \acc -> f x:acc) [] ys)
          *Main| :}
          *Main> map' (^2) [1..10]
          [1,4,9,16,25,36,49,64,81,100]
          
          *Main> :{
          *Main| filter' :: (a -> Bool) -> [a] -> [a]
          *Main| filter' = \p -> \ys -> (foldr (\x -> \acc -> if p x then x:acc else acc) [] ys)
          *Main| :}
          *Main> filter' (>10) [1..100]
          

          在上面的sn-ps中,acc指的是累加器,x指的是最后一个元素。

          【讨论】:

            猜你喜欢
            • 2019-09-04
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2016-06-25
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2019-08-26
            相关资源
            最近更新 更多