【问题标题】:How would I implement this fold function?我将如何实现这个折叠功能?
【发布时间】:2017-07-04 22:12:16
【问题描述】:

有两种数据类型 Color 和 Plant。

data Color = Red | Pink | White | Blue | Purple | Green | Yellow
   deriving (Show, Eq)

data Plant =
     Leaf
   | Blossom Color
   | Stalk Plant Plant
   deriving (Show, Eq)

现在我应该实现以下类型的函数fold_plant

(x -> x -> x) -> (Color -> x) -> x -> Plant -> x

我理解折叠函数的方式是它接受一个列表,并且对于每次迭代,它都会从列表中删除第一个元素并对该元素执行一些操作。

显然fold_plant Stalk Blossom Leaf是植物上的标识。

现在我知道在 Haskell 中你可以创建这样的函数:

fold_plant :: (x -> x -> x) -> (Color -> x) -> x -> Plant -> x
fold_plant = do something

但从这里开始,我不知道折叠功能如何在植物中发挥作用。

【问题讨论】:

  • 你自己尝试过什么?此外,我想fold 会从左到右工作吗?
  • 啊,好古老的变质。简而言之,用通用函数(或常量)“替换”每个构造函数。请注意,您恰好有 3 个参数(+ 植物)和 3 个构造函数。将参数命名为leaf blossom stalk,以便让它们替换的内容一目了然。添加一些递归。
  • @WillemVanOnsem 这里没有从左到右或从右到左,它不是列表,也不是像Foldable 那样访问的ADT。这是一个简单的变质。

标签: haskell tree fold catamorphism


【解决方案1】:

如果我们看一下函数签名:

fold_plant :: (x -> x -> x) -> (Color -> x) -> x -> Plant -> x
--            \_____ _____/    \____ _____/    |      |      |
--                  v               v          v      v      v
--                stalk           blossom     leaf  tree  output

我们看到有stalk 部分以及blossom 部分和leaf 部分。我们将在这里命名stalk 函数sblossom 函数bleaf 部分l。为了简化(和优化)函数,我们将这三个参数解包,然后调用一个递归方法:

fold_plant s b l = fold_plant'
    where fold_plant' = ...

现在的问题当然是如何处理fold_plant'。给定我们看到Leaf,我们不需要对这些值执行任何操作,我们只需返回我们的叶子结果l

fold_plant' Leaf = l

如果我们找到(Blossom c)c 一种颜色,我们必须使用b 部分执行从c :: Colorx 的映射以获得新值:

fold_plant' (Blossom c) = b c

最后,如果我们有一个Stalk,我们将不得不执行递归:我们首先在左边的茎上调用fold_plant',然后我们调用fold_plant',并用两个结果构造一个s

fold_plant' (Stalk lef rig) = s (fold_plant' lef) (fold_plant' rig)

所以我们可以把它们放在下面的函数中:

fold_plant s b l = fold_plant'
    where fold_plant' Leaf = l
          fold_plant' (Blossom c) = b c
          fold_plant' (Stalk lef rig) = s (fold_plant' lef) (fold_plant' rig)

【讨论】:

  • 首先,感谢您的出色回答。我还不明白这个折叠功能究竟会做什么。函数的示例调用会是什么样子?例如,我将如何使用此函数来获取植物中的叶子数量?因为我不能只给植物折叠功能
【解决方案2】:

折叠是一个函数,它获取结构中的一段数据,并将其折叠为另一段数据。通常我们这样做是为了将集合“减少”为单个值。这就是为什么如果您查看其他语言,如 Lisp、Smalltalk、Ruby、JavaScript 等,您会发现这个名为 reduce 的操作,它是 Haskell 中折叠的可怜表亲。

我说这是可怜的表弟,因为您对列表的直觉是正确的,但是在 Haskell 中,我们更加抽象和通用,因此我们的折叠函数可以在任何类型的结构上工作,我们已经告诉 Haskell 的类型是什么折叠意味着为。

因此,我们可以谈论“使用加法和折叠将数字列表转换为和值”,或者我们可以谈论“使用函数获取名称的家谱并将其折叠成列表”,等等等等。每当我们有将某物的结构更改为单个值或可能更改为不同的结构化值集的想法时,这就是折叠。

在 Haskell 中表示这一点的“规范”方式是 foldr :: Foldable t => (a -> b -> b) -> b -> t a -> b,但它更容易,就像您认为在开头使用“a”作为 Foldable f => t a 类型一样,因为这样更容易理解。所以我们有一个专门的类型foldr :: (a -> b -> b) -> b -> [a] -> b。但是 ab 是什么? (a -> b -> b) 是什么,这三个参数有什么作用?

让我们将它专门用于Int 值,用于abfoldr :: (Int -> Int -> Int) -> Int -> [Int] -> Int 哇...这使它...有趣不是吗?所以foldr 采用两个整数的函数,例如(+) 函数,它采用单个Int 值(这是它将用作目标结果的初始值,以及@987654337 的列表@values... 然后它将产生一个 Int... 也就是说,它采用 Int -> Int -> Int 函数并将其应用于单个 Int 和第一个 [Int],然后应用那个函数到那个结果和[Int]的下一个值等等,直到没有更多的[Int]离开......然后这就是它返回的内容。

实际上是在数据结构上折叠函数。

这对列表来说很好,它是一条直线,但你所拥有的是一棵树,而不是列表。那么它是如何在那里工作的呢?好吧,让我们看看我们如何专门化foldr 以从Int 的列表中生成一对最高和最低数字? foldr :: (Int -> (Int, Int) -> (Int, Int)) -> (Int, Int) -> [Int] -> (Int, Int)。所以我们采用一个函数,它接受一个Int 和一对,我们将初始对以及来自[Int] 的第一个Int 放入其中。这会返回给我们一个新的配对,然后我们对[Int]下一个 执行相同的过程,然后我们继续该过程,直到最后只剩下一对。

foldToMinMax = foldr (\newNum (minnum,maxnum) -> (min minnum newNum, max maxnum newNum)) (maxBound :: Int, minBound :: Int)

所以现在事情变得有点更清楚了。

不过,你的这棵花树呢?好吧,您需要为自己编写一个折叠函数,该函数将采用两个值,其中一个与初始值和结果值相同,另一个是您的树的类型,并构建一个值结果类型的。如果我要使用伪代码以更具描述性的方式编写类型,我可能会编写如下内容:foldr :: (contentOfCollectionType -> resultType -> resultType) -> resultType -> (collectionWrapper contentOfCollectionType) -> resultType

但是你不必在这里使用foldr,事实上你不能使用它,除非你做一些花哨的类型类实例化的东西。您完全可以使用普通递归编写自己的折叠函数。这就是他们所追求的。

如果你想学习递归和折叠之类的,但你还不了解这些东西,我推荐我帮助编写的书。 http://happylearnhaskelltutorial.com 它更详细地解释了它,并提供了许多清晰的示例。如果您了解基础知识,应该很快就可以快速了解您想了解递归和折叠的点......但是如果您不了解,那么了解基础知识,因为您需要在了解其他内容之前了解它们。

我应该提到你的特定折叠也有一个转换功能。它是将Color 转换为x 的东西。您作为折叠函数使用的函数“将 x 压在一起”(即采用两个 x 值并产生另一个 x 值,与上面示例中的 (+) 非常相似)。它只能在树上工作,因为我们还为它提供了将 Color 转换为 x 的函数,这有效地将有意义的数据从树中取出,并将其放入折叠函数可以使用的形式中。

这里有一个非常漂亮的模式。

祝你好运!

【讨论】:

    【解决方案3】:

    折叠是递归问题解决的本质:

        data Plant =                        data Result r =    
             Leaf                                RLeaf 
           | Blossom Color                     | RBlossom Color
           | Stalk Plant Plant                 | RStalk r r
                -- recursive data           -- non-recursive data: `r`, not `Result r`!

    递归问题解决是关于以一种简单的方式递归处理原始结构的组成部分的结果:

        -- single-step reduction semantics:
        -- reduction_step :: ..... -> Result r -> r
        reduction_step :: (r -> r -> r) -> (Color -> r) -> r -> Result r -> r
        reduction_step s b l  RLeaf       = l
        reduction_step s b l (RBlosom c)  = b c
        reduction_step s b l (RStalk x y) = s x y

    但是要达到这一点,我们需要递归到与整个结构具有相同性质的原始结构的组成部分,因此我们寻求创建的fold_plant 过程可以应用于它们如果已经写好了(递归!):

        recurse_into :: (r -> r -> r) -> (Color -> r) -> r -> Plant -> Result r
        recurse_into s b l Leaf          = RLeaf
        recurse_into s b l (Blossom c)   = RBlossom c
        recurse_into s b l (Stalk lt rt) = RStalk (fold_plant s b l lt) (fold_plant s b l rt)

    所以,最后,我们的折叠只是两者的组合,

        fold_plant :: (r -> r -> r) -> (Color -> r) -> r -> Plant -> r
        fold_plant s b l plant = reduction_step s b l          --          Result r -> r
                                   (recurse_into s b l plant)  -- Plant -> Result r

    请遵循类型并说服自己,一切都应符合要求。

    当然可以避免临时数据定义和折叠函数定义,但这是要遵循的一般过程。

    (参见递归方案)。

    【讨论】:

      猜你喜欢
      • 2014-09-04
      • 2018-10-15
      • 2022-07-28
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2021-11-23
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多