问题描述
我正在研究符号代数系统。我目前正在研究如何从二叉分析树执行多项式加法/乘法。稍后我会认为这是一个普通戒指。
解析在这里不相关——这旨在成为解析的输出。形成的解析树。如果这里有什么可以改进的地方,我当然也很高兴了解这一点。
我“感觉”这个树结构可以折叠/压碎,但我不太清楚如何去做。我相信我可以一起破解一些东西,但由于我仍在学习 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
解决方法
“折叠”在常用语中有两个相关但截然不同的含义。
-
Fold
与Foldable
中的一样。将数据类型视为容器,Foldable
使您可以访问容器的元素,同时丢弃有关容器本身形状的任何信息。 (这也是lens
的Fold
使用该术语的意义。)并非所有数据类型都是Foldable
— 只有那些可以被视为容器的数据类型。 - “Fold”也可以表示“catamorphism”,这是一种编写高阶函数的技术,可将结构简化为摘要值。每个数据类型都有一个对应的 catamorphism,但 catamorphism 的签名取决于数据类型。
当您谈论的数据类型是 []
时,“fold”的两种含义碰巧重合(这部分解释了对两种含义的混淆),但通常情况下并非如此。您的 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
的一个方法),在第二个子句中我在两种不同的类型中使用它。foldr
中的foldTree
是对Tree
的foldr :: (a -> b -> b) -> b -> Tree a -> b
定义的递归调用。上面一行中的foldr
是对普通列表foldr :: (a -> b -> b) -> b -> [a] -> b
的调用。 -
foldr
丢弃信息!在Node
的情况下,您可以从_
看到Op
掉在地板上。我们还将ls
和rs
粉碎到一个列表中,忘记了它们曾经在两个列表中。
所以 Foldable
就是在容器内查找项目,同时丢弃有关容器本身的任何信息。说白了,Foldable
就是“toList
-as-a-class”。
Foldable
非常适合容器类型和其他抽象数据类型,您希望在其中公开类似集合的接口,同时隐藏数据结构的内部表示。在您的情况下,您提到您想根据树内的 +
应用操作 *
或 Op
,但 Op
被丢弃 来自 Foldable
。因此,Foldable
似乎不是您想要做的事情的正确工具。
如果你想将你的数据类型减少到一个汇总值而不丢弃关于结构的任何信息,你需要一个catamorphism。
要导出 catamorphism 的签名,请遵循此配方。
-
cata
获取您的数据类型的值并返回汇总值b
。 - 您的数据类型的每个构造函数都对应于
cata
的一个参数。每个参数都是一个返回b
的函数。 - 数据类型构造函数的每个字段对应于相应函数的参数之一。
- 数据类型本身的递归提及成为汇总值
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
现在让我们看看 Node
。 Node
有三个参数,但其中两个是 Tree
的列表。我们想用相应的汇总值替换这些 Tree
中的每一个。所以 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)