【问题标题】:Most effective way to fold polynomial parse tree折叠多项式解析树的最有效方法
【发布时间】:2021-05-03 11:19:39
【问题描述】:

我正在研究一个符号代数系统。我目前正在研究如何从二叉解析树执行多项式加法/乘法。我稍后会认为这是一个通用的戒指。

解析与此处相关——这旨在成为解析的输出。形成的分析树。如果这里有什么可以改进的地方,我当然也很高兴知道这一点。

我“感觉”这个树形结构可以折叠/压碎,但我不太清楚如何去做。我相信我可以一起破解一些东西,但是由于我还在学习 Haskell,所以我想了解更高级的用户会做什么。

以下是相关代码背景。

我的两个相关数据声明是:

data Op = Add | Mul
data Tree a = Leaf a | Node Op [Tree a] [Tree a]

这是我的一个测试示例:

-- 3*(x+2)*(x+(5*4))
test = Node Mul [
         Node Mul 
          [Leaf "3"] 
          [Node Add 
            [Leaf "x"] 
            [Leaf "2"]
          ]
         ] 
         [Node Add 
           [Leaf "x"] 
           [Node Mul 
             [Leaf "5"] 
             [Leaf "4"] 
           ]
         ]

这是一种典型的递归树类型,一个节点包含一个操作以及左右树的列表。计算通过以下操作进行。注意:它们现在都是字符串操作。我需要它们尽可能通用(但稍后会添加进一步的结构,例如乘法交换性)。

prod :: [Tree [a]] -> [Tree [a]] -> [Tree [a]]
prod ls rs = [Leaf (l ++ r) | (Leaf l) <- ls, (Leaf r) <- rs]

add :: [Tree [a]] -> [Tree [a]] -> [Tree [a]]
add l r = l ++ r

opOnLeaf :: Op -> [Tree [a]] -> [Tree [a]] -> [Tree [a]]
opOnLeaf op l r  
  | op == Add = add l r
  | op == Mul = prod l r

operate :: Tree [a] -> [Tree [a]]
operate (Node op [Node op2 l2 r2] [Node op3 l3 r3]) = operate (Node op (operate (Node op2 l2 r2)) (operate (Node op3 l3 r3)))
operate (Node op [Node op2 l2 r2] r) = operate (Node op (operate (Node op2 l2 r2)) r)
operate (Node op l [Node op2 l2 r2]) = operate (Node op l (operate (Node op2 l2 r2)))
operate (Node op l r) = opOnLeaf op l r

我认为我在operate 中的变量名很可怕,但我想确保它首先起作用。让它变得丑陋也使它更加突出。它实际上是在对我大喊大叫,以找到更好的方法。

现在,我想使用 fold 或 foldMap,但我遇到了一个问题,即我在每个操作中都有不同的幺半群(即 (*)(+)。这只是开始——我将添加其他几个操作。所以我需要类似“变量幺半群”的东西,或者可能是其他一些地图,其操作是首先获取幺半群。我不确定。

查询:给定这棵二叉树,以及每个节点上的多个操作,如何继续折叠结构?有没有更好的方法把它写成可折叠的实例或其他结构?我还打算添加其他几个二进制操作。理想情况下,我想要一个可以无限扩展的结构。我假设一旦解决了 2,我们就可以有效地解决 n。

Tree drawing corresponding to test example

【问题讨论】:

  • 你为什么使用Node构造函数的列表,Node Op (Tree a) (Tree a)不应该更有意义吗?
  • 很有可能。到目前为止,这是我的理由。 (1) 在每个操作应用程序(例如,Add)之后,每个子树都归结为一个列表。我想在前进时将 [Leaf] 类型向上携带; (2)在某些情况下,解析后,我希望有多个值开始,比如:[Leaf 2, Leaf 3, Leaf 4],如果我想表示 2 + 3 + 4。所以我'我使用叶子列表,我使用树列表。然而,我认为这个理由不太令人信服(我正在写这篇临时文章);如果它们总是二进制的,那么没有节点列表是有意义的。也许我需要更多类型。
  • 在较高级别的foldr 中,您将一个运算符(例如:)更改为另一个运算符,并且所有叶子 都具有匹配的值。所以在这里你需要像foldTree :: ([acc] -&gt; [acc] -&gt; Op -&gt; acc) -&gt; acc -&gt; Tree a -&gt; acc(left-accs,right-accs,operator -> new acc)这样的东西 - 我想你可以实现这个,但我不确定这是否有帮助
  • (1) 所以把它简化为一个列表——而不是一个树列表。写prod :: Tree a -&gt; Tree a -&gt; [[a]]等,而不是你这种疯狂的类型。 (2) 将其解析为Node Add (Leaf 2) (Node Add (Leaf 3) (Leaf 4))
  • 如果要将运算符与列表相关联,则为每个Node 一个列表而不是两个。

标签: parsing haskell fold parse-tree


【解决方案1】:

“Fold”在常用语中具有两个相关但截然不同的含义。

  1. FoldFoldable 相同。将数据类型视为容器,Foldable 让您可以访问容器的元素,同时丢弃有关容器本身形状的任何信息。 (这也是lensFold 使用该术语的意义。)并非所有数据类型都是Foldable — 只有那些可以被视为容器的数据类型。
  2. “Fold”也可以表示“catamorphism”,这是一种编写高阶函数的技术,可将结构简化为汇总值。每个数据类型都有相应的变态,但变态的签名取决于数据类型。

当您谈论的数据类型是[] 时,“折叠”的两个含义恰好重合(这部分解释了对这两个含义的混淆),但通常它们不会。您的Tree 恰好是任何一种弃牌的合适人选,从您的问题来看,我无法完全确定您要的是哪一种,所以我将向您展示如何同时做到这一点。


编写Foldable 实例的最简单方法是打开DeriveFoldable

{-# LANGUAGE DeriveFoldable #-}
data Tree a = Leaf a | Node Op [Tree a] [Tree a]
  deriving (Foldable)

但为了便于理解,我将在此处写出实例:

instance Foldable Tree where
    -- foldr :: (a -> b -> b) -> b -> Tree a -> b
    foldr f z (Leaf x) = f x z
    foldr f z (Node _ ls rs) = foldr foldTree z (ls ++ rs)
        where foldTree t z' = foldr f z' t

这个想法是遍历树到底部,将f 应用于每个Leaf 中的值。关于这段代码需要注意的几点:

  • foldr 是一个重载函数(它是Foldable 的一个方法),在第二个子句中,我将它用于两种不同的类型。 foldTree 中的foldr 是对Treefoldr :: (a -&gt; b -&gt; b) -&gt; b -&gt; Tree a -&gt; b 定义的递归调用。上面一行中的foldr 是对通常列表foldr :: (a -&gt; b -&gt; b) -&gt; b -&gt; [a] -&gt; b 的调用。
  • foldr 丢弃信息!在Node 的情况下,您可以从_ 中看到Op 掉在了地板上。我们还将lsrs 合并到一个列表中,忘记了它们曾经在两个列表中。

所以Foldable 就是要找到容器内的物品,同时丢弃有关容器本身的任何信息。说白了,Foldable 就是“toList-as-a-class”。

Foldable 非常适合容器类型和其他抽象数据类型,您希望在其中公开类似集合的接口,同时隐藏数据结构的内部表示。在您的情况下,您提到您想根据树内的Op 应用操作+*,但OpFoldable丢弃。所以看起来Foldable 不是你想要做的事情的正确工具。


如果你想将你的数据类型减少到一个汇总值而不丢弃任何关于结构的信息,你需要一个catamorphism

要导出变质的特征,请遵循这个秘诀。

  1. cata 采用您的数据类型的值并返回摘要值 b
  2. 数据类型的每个构造函数都对应于cata 的一个参数。这些参数中的每一个都是一个返回b的函数。
  3. 数据类型构造函数的每个字段都对应于相应函数的参数之一。
  4. 数据类型本身的递归提及成为汇总值b

让我们为您的Tree 执行以下步骤。首先,cataTree 接受一个Tree 并返回一个汇总值b

cataTree :: ??? -> Tree a -> b

查看Tree 的定义,我们看到两个构造函数。所以cataTree 将有两个函数参数,一个用于Leaf,一个用于Node

cataTree :: ({- Leaf -} ??? -> b) -> ({- Node -} ??? -> b) -> Tree a -> b

查看Leaf 构造函数,我们看到一个字段。所以Leaf 函数只有一个参数。

cataTree :: (a -> b) -> ({- Node -} ??? -> b) -> Tree a -> b

现在让我们看看NodeNode 有三个参数,但其中两个是 Trees 的列表。我们想用相应的汇总值替换每个Trees。所以cataTree 的最终签名是

cataTree :: (a -> b) -> (Op -> [b] -> [b] -> b) -> Tree a -> b

实现cataTree 是遵循类型的问题。

cataTree leaf _ (Leaf x) = leaf x
cataTree leaf node (Node op ls rs) =
    node op (fmap (cataTree leaf node) ls) (fmap (cataTree leaf node) rs)

【讨论】:

    猜你喜欢
    • 2010-10-11
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多