【问题标题】:How to implement a generic hierarchy of structures with injected functionality如何实现具有注入功能的通用结构层次结构
【发布时间】:2011-08-23 07:55:51
【问题描述】:

我想为树结构实现一个通用层次结构,以后可以以独立于实现的方式使用它来描述树上的通用算法。

我从这个层次结构开始:

interface BinaryTree<Node> {
    Node left(Node);
    bool hasLeft(Node);

    Node right(Node);
    bool hasRight(Node);
}

interface BinaryTreeWithRoot<Node> : BinaryTree<Node> {
    Node root();
}

interface BinaryTreeWithParent<Node> : BinaryTree<Node> {
    Node parent(Node);
    bool hasParent(Node);
}

现在,基本上我希望能够以一种通用的方式实现子树的概念: 对于每个类 T : BinaryTree,我想要一个“类” Subtree(T),它提供与 T 相同的功能(因此它必须从它派生),并且还重写了 root() 功能。

类似这样的:

class Subtree<T, Node> : T, BinaryTreeWithRoot<Node> 
    where T : BinaryTree<Node>
{
    T reference;
    Node root;

    void setRoot(Node root) {
        this.root = root;
    }

    override Node BinaryTreeWithRoot<Node>::root() {
        return this.root;
    }

    // Now, inherit all the functionality of T, so an instance of this class can be used anywhere where T can.
    forall method(arguments) return reference.method(arguments);
}

现在有了这段代码,我不确定如何创建子树类型的对象,因为树对象应该以某种方式被注入。

一种方法是为我创建的每个树类创建一个子树类,但这意味着代码重复,而且毕竟是同一件事。

因此,一种方法是 mixins,它允许泛型类从其模板参数派生。

我也对如何在 Haskell 中实现这样的层次结构感兴趣,因为 Haskell 有一个很棒的类型系统,我认为注入这样的功能会更容易。

例如在 Haskell 中可能是这样的:

class BinaryTree tree node where
    left :: tree -> node -> node
    right :: tree -> node -> node

class BinaryTreeWithRoot node where
    left :: tree -> node -> node
    right :: tree -> node -> node -- but this is a duplication of the code of BinaryTree
    root :: tree -> node

instance BinaryTree (BinaryTreeWithRoot node) where
    left = left
    right = right

data (BinaryTree tree node) => Subtree tree node = 
 ...

instance BinaryTreeWithRoot (Subtree tree node) where ...

我很感兴趣是否以及如何在 oop 语言(c++、c#、d、java)中完成此操作,因为 c++ 和 d 提供开箱即用的 mixins(我不确定 d),出于对 Haskell 类型系统的好奇。

【问题讨论】:

  • 在 C# 中,您不能从泛型类型参数继承。您可以实现BinaryTreeWithRoot&lt;Node&gt; 接口并将BinaryTree&lt;Node&gt; 调用链接到传递的referenceT
  • 但这意味着对于每个可能的 BinaryTreeXXX 功能,我应该专门化通用子树并链接所有功能: class Subtree where T : BinaryTree1 ... { all the methods BinaryTree1 } class Subtree where T : BinaryTree2 ... { BinaryTree2 的所有方法 } 等等...
  • true,所以SubTree 应该实现BinaryTreeWithRoot&lt;T&gt; 而不是BinaryTreeWithRoot&lt;Node&gt;
  • 我不太明白如何做到这一点。如果您建议使 BinaryTreeXXX 接口更通用,例如:pastebin.com/jSdcYYuS,那么我看不到特定的子树将如何映射到 BinaryTree 接口的特定实现。那是因为子树现在有方法,这些方法将树作为参数。此外,由于 C# 没有 typedef,我们应该显式传递所有模板类型参数以正确描述它们的条件,这对于(间接)依赖于许多模板参数的类来说将变得非常不方便。

标签: c# c++ haskell d


【解决方案1】:

由于 D 有“真正的”模板,而不是泛型,所以让模板类从其模板参数继承是微不足道的:

class A {}
class B(T) : T {
    static assert(is(B!T : T));  // Passes.
}

就使Subtree 在 D 中工作而言,应该这样做,假设您还有一个模板类 Node

class Subtree(T) : T, BinaryTreeWithRoot!(Node!(T))
{
    T reference;
    Node root;

    void setRoot(Node root) {
        this.root = root;
    }

    override Node root() {
        return this.root;
    }
}

但是,IIUC(如果我错了,请纠正我),T 是树的有效负载,因此可能是一个原语。如果是这种情况,您最好能够通过alias thisSubtree!(T) 用作T,这允许在没有继承的情况下进行子类型化并使用原语:

class Subtree(T) : BinaryTreeWithRoot!(Node!(T))
{
    T reference;
    alias reference this;  // Make this implicitly convertible to reference.
    Node root;

    void setRoot(Node root) {
        this.root = root;
    }

    override Node root() {
        return this.root;
    }
}

【讨论】:

  • 我知道 D 很强大,但我对它的理解还处于基本水平。我会检查“别名”和其他内容。我可以忍受“节点”是一个模板类。
【解决方案2】:

在 Haskell 中创建这样的树形界面是……不寻常的。 NodeSubtree 都是多余的。这部分是由于代数类型,部分是因为 Haskell 数据是不可变的,因此需要不同的技术来完成某些事情(例如设置根节点)。可以这样做,界面看起来像:

class BinaryTree tree where
    left :: tree a -> Maybe (tree a)
    right :: tree a -> Maybe (tree a)

-- BinaryTreeWithRoot inherits the BinaryTree interface
class BinaryTree tree => BinaryTreeWithRoot tree where
    root :: tree a -> tree a

然后,使用非常标准的二叉树定义:

data Tree a =
  Leaf
  | Branch a (Tree a) (Tree a)

instance BinaryTree Tree where
  left Leaf = Nothing
  left (Branch _ l r) = Just l
  right Leaf = Nothing
  right (Branch _ l r) = Just r

data TreeWithRoot a =
  LeafR (TreeWithRoot a)
  | BranchR a (TreeWithRoot a) (TreeWithRoot a) (TreeWithRoot a)

instance BinaryTree TreeWithRoot where
-- BinaryTree definitions omitted

instance BinaryTreeWithRoot TreeWithRoot where
  root (LeafR rt) = rt
  root (BranchR _ rt l r) = rt

由于此接口返回Maybe (tree a),因此您也可以使用leftright 来检查分支是否存在,而不是使用单独的方法。

这并没有什么特别的问题,但我相信我从未见过有人真正实施过这种方法。更常用的技术是根据FoldableTraversable 定义遍历或创建zipper。拉链很容易手动派生,但有几种通用的拉链实现,例如zipperpezsyz

【讨论】:

  • 我喜欢这种方法。它更简单,因为它消除了节点和子树。我将看看 zipper 和 multirec 包,因为这可能正是我所需要的(对于 Haskell 实现)。我问这个问题是因为我想描述独立于树结构的树上的各种算法,但我不确定如何使它们尽可能通用。
  • 例如,假设我想要以下“能力”(构建一个分段树):给定一个形状不可变的树 T,其节点包含 Monoid 的元素。令 L 为叶子的集合,并让这个集合有序(所以在叶子上我有一个静态(不改变其大小)的幺半群元素序列。想要支持以下查询:get::leaf a - > leaf b -> monoid,获取 mapply 在 [a,b] 范围内的累积值,并更改 :: tree -> Leaf -> monoid -> tree,更改叶子处的值。
  • 此功能实现起来非常简单,并且需要具有父概念的二叉树可用。我希望能够独立于具体的树实现来描述算法,而我提出的这些类是为了“插入”一个通用树,它可以以各种方式实现(例如:树可以表示为一个列表,左右函数将在列表中占据位置p,并返回2*p和2*p+1,这是完全合理的树。
  • 听起来确实像拉链是您在 Haskell-land 中寻找的东西。更重要的是,拉链实现了对任何容器数据类型的通用遍历,而不仅仅是树。这种类型类方法也应该有效,除非 Landei 指出您需要小心父链接,因为它们会创建循环数据结构。
  • @comco - 这可能会占用大量空间,并且如果树的形状发生变化,那么计算所有内容将是大量工作。对于一个可能没问题的特殊用途树实现,但我怀疑它是否能正常工作。
【解决方案3】:

在 C# 4 中,我会使用动态来实现这个目标。例如,您可以尝试将 SubtTree 类定义为:

public class Subtree<T, Node> : DynamicObject, BinaryTreeWithRoot<Node> where T : BinaryTree<Node>
{
    private readonly T tree;

    public Subtree(T tree)
    {
        this.tree = tree;
    }
}

并使用树的方法/属性覆盖 DynamicObject 的适当方法。更多信息(和示例代码)可以在这篇关于 Using C# 4.0 dynamic to drastically simplify your private reflection code 的精彩博客文章中找到。

值得一提的是,由于使用了动态能力和反射,会引入小的性能开销以及降低安全性(因为它可能涉及违反封装)。

【讨论】:

  • 好点,好链接。但值得指出的是,您将在安全性和性能方面付出(小)代价。
  • @dema80:感谢您的意见,我已按照建议修改了答案
【解决方案4】:

我认为通过“BinaryTree”的方法假设了太多的固定结构,并且不必要地以非通用方式定义了您的接口。当您的树扩展到非二进制形式时,这样做会使重用算法变得困难。相反,当没有必要或无用时,您需要为多种样式编写接口。

此外,使用 hasLeft/hasRight 检查进行编码意味着每次访问都是一个两步过程。检查固定位置的存在不会提供有效的算法。相反,我认为您会发现添加可能是二进制左/右或二进制红/黑或字符索引或其他任何东西的通用属性将允许更多地重用您的算法并检查数据是否只能由这些算法完成需要它(特定的二进制算法)。

从语义上看,你想先编码一些基本属性,然后再进行专业化。当您“位于”算法中的某个节点时,您希望能够首先找到子 。这应该是边缘结构的容器范围,允许您导航到子节点。由于它可以是一个通用容器,它可以有 0、2、5、1 甚至 100 条边。许多算法并不关心。如果它为 0,则在该范围内迭代将无济于事 - 无需检查 hasX 或 hasY。对于每条边,您应该能够获取孩子的节点,并递归任何您想要的算法。

这基本上是 boost 在其 Graph 库中采用的方法,它允许将树算法扩展到适用的图形,以实现更好的通用算法重用。

所以你已经有了一个基本的界面

TreeNode:
  getChildEdges: () -> TreeEdgeRange

TreeEdge:
  getChildNode: () -> TreeNode

以及您喜欢的语言喜欢的任何范围到对象。例如,D 有一个特别有用的范围语法。

你会想要一些基本的树对象来给你节点。类似的东西

Tree:
  getTreeNodes: () -> TreeNodeRange

让你开始。

现在,如果您想支持 BinaryTrees,请将此作为对该接口的限制。请注意,您实际上并不需要任何新的接口方法,您只需要强制执行更多不变量 - 每个 TreeNode 都有 0、1 或 2 个 childEdge。只需创建一个指示此语义限制的接口类型:

BinaryTree : Tree

如果你想支持有根树,添加一个接口层

RootedTree : Tree:
  getRoot: () -> TreeNode

添加该功能。

基本思想是,如果您要使类在层次结构中更加具体,则不必添加接口方法来添加语义要求。仅当有需要访问的新语义行为时才添加接口方法。否则 - 在通用接口上强制执行新的不变量。

最终,您需要使用包含节点或边的数据的结构来装饰节点和边,这样您就可以构建 Tries 和红黑树以及所有高级算法的出色工具。所以你会想要拥有

PropertiedTreeNode<Property> : TreeNode:
  getProperty: () -> Property

PropertiedTreeEdge<Property> : TreeEdge:
  getProperty: () -> Property

由于这是您希望允许通用算法处理的事情,因此属性是否是树的一部分的类型信息应该是通用的,并且算法可以忽略。这使您进入了 boost 的设计轨道,这些问题已经得到了非常优雅的解决。如果您想了解如何构建通用树算法库,我建议您研究该库。

如果您遵循上述类型等同于语义描述的准则,那么您的子树应该是显而易见的 - 它与它所来自的树的类型完全相同!事实上,你真的根本不应该有 SubTree 类型。相反,您应该只拥有您正在处理的特定 TreeNode 类型的方法

PropertiedTreeNode<Property>:
  getSubTree: () -> PropertiedTree<Property>

而且,与 boost 一样,当您在 Tree 的通用属性中编码更多有关 Tree 功能的信息时,您可以获得具有更广泛接口协定的新 Tree 类型。

【讨论】:

  • 感谢您的出色回答。我认为将这些 ::boost 概念与 d 编程语言相结合将是可行的方法。
【解决方案5】:

正如您所指出的,一种方法是为我创建的每个树类创建一个子树类, 这意味着代码重复,但可以使用反射和 T4 以某种方式“避免”,或者更好地自动化。我自己为过去的一个项目做的,效果很好!

您可以从 Oleg Synch 博客开始了解 T4 的概述。这里是自动生成类的一个很好的例子:http://www.olegsych.com/2007/12/how-to-use-t4-to-generate-decorator-classes/

【讨论】:

  • 好的。我猜在 C# 中最好的方法是这样的(反射)或使用表达式。其他语言呢?坏事是在编译时不能严格检查使用反射。最终,我想要一个严格的编译时替代方案(c++ 的缺点是没有检查泛型,你不能对它们设置条件,这在现实生活中意味着奇怪的错误和糟糕、糟糕的错误消息。)
  • 您确实可以在 C++ 中编写模板 class Subtree : public T ...,但不能在 C# 中编写(正如您正确指出的那样)。我将它用于“自动单身人士”,就像你可以在这里找到的那​​样 scottbilas.com/publications/gem-singleton
【解决方案6】:

你可以这样做:-

public class Node {
    public Node Left {get; set:}
    public Node Right {get; set;}
    public Node Parent {get; set;}  // if you want to be able to go up the tree
    public Node Root {get; set;}    // only if you want a direct link to root
}

子树只是一棵树,每棵树都可以表示为该树的根节点,然后该节点可以具有能够在树中导航的属性。

将其设为通用 Node&lt;T&gt; 并存储该值。如果您不喜欢公共设置器,请将它们设为私有并仅在构造函数或某些安全的AddLeft(...) 等方法中设置它们。

您也可以摆脱Root,只需遍历Parent 链接,直到找到一个空的Parent 值(或到达您的子树案例的顶部节点)。

【讨论】:

  • 我可以看到这种方法的两个问题: 1. 在这个 Node 类中,我们假设我们可以访问父节点并且树有一个根。但是例如,如果树节点被定义为: struct node { node* left, *right; };现在,在一个特定的节点上,我们没有向上 og 的信息。另一件事是某些算法不“要求​​”树节点可以访问父节点来工作。这就是为什么我想在 BinaryTree 而不是 BinaryTreeWithRoot 上实现它们。
  • 另一件事是,有时节点信息本身不足以在没有树本身的情况下“标记”树中的位置。例如,我想允许压缩树(即一个数组,从 1 开始索引,长度为 n,其中“节点”只是一个索引,左(节点)操作是 (0 + 2*node) 和右(node) 是 (1 + 2*node)。也就是说,将所有本地信息放在一个节点上并不适用于所有场合。
  • 指向 parent 的指针会在纯语言中引起问题,例如在哈斯克尔。是的,您可以以某种方式击败循环依赖项(多亏了懒惰),但它变得非常丑陋。
  • @Landei:非常正确。这是一个简单的结,但是像Eq 这样的事情会变得很讨厌。拉链更容易使用。
猜你喜欢
  • 2016-04-12
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-09-27
相关资源
最近更新 更多