Java中的ADT类多态(不改变类)
在Haskell中,我可以定义以下数据类型:
data Tree = Empty | Leaf Int | Node Tree Tree
然后写这样的多态函数:
depth :: Tree -> Int depth Empty = 0 depth (Leaf n) = 1 depth (Node lr) = 1 + max (depth l) (depth r)
在Java中,我可以使用接口模拟代数数据类型:
interface Tree {} class Empty implements Tree {} class Leaf implements Tree { int n; } class Node implements Tree { Tree l; Tree r; }
但是,如果我尝试使用类似Haskell的多态,我会收到一个错误:
int depth(Empty node) { return 0; } int depth(Leaf node) { return 1; } int depth(Node node) { return 1 + Math.max(depth(node.l), depth(node.r)); // ERROR: Cannot resolve method 'depth(Tree)' }
克服这个问题的正确方法是将方法 depth()
放到每个类中 。 但是,如果我不想把它放在那里怎么办? 例如,方法depth()
可能与Tree
没有直接关系,将它添加到class会破坏业务逻辑。 或者,更糟糕的是, Tree
可能写在我无法访问的第三方库中。 在这种情况下,实现类似ADT的多态性的最简单方法是什么?
为了以防万一,目前我正在使用以下语法,这显然是不受欢迎的:
int depth(Tree tree) { if (tree instanceof Empty) depth((Empty)tree) if (tree instanceof Leaf) depth((Leaf)tree); if (tree instanceof Node) depth((Node)tree); else throw new RuntimeException("Don't know how to find depth of " + tree.getClass()); }
尝试这样的事情。
对不起,我的Java非常生疏。 如果,与我不同,您可以记住语法,您可以使用Javagenerics来将Object
为Integer
或您正在编写的方法所需的任何类。 但你不能(你能吗?)返回原始类型,抱歉。
interface TreeFolder { Object onEmpty(); Object onLeaf (int n); Object onNode (Tree l, Tree r); } interface Tree { Object fold (TreeFolder f); } class Empty implements Tree { Object fold (TreeFolder f) { return f.onEmpty(); } } class Leaf implements Tree { private int n; Object fold (TreeFolder f) { return f.onLeaf (n); } } class Node implements Tree { private Tree l, r; Object fold (TreeFolder f) { return f.onNode (l, r); } } // meanwhile, in a class in another package far far away... Object depth (Tree tree) { return tree.fold (new TreeFolder() { Object onEmpty() { return new Integer(0); } Object onLeaf (int n) { return new Integer(n); } Object onNode (Tree l, Tree r) { Integer ll = (Integer) l.fold (this); Integer rr = (Integer) r.fold (this); return new Integer (ll.intValue() + rr.intValue()); } }); }
请注意,在depth()
我必须在Tree
参数上手动递归(调用fold()
)。 您可以选择在Node.fold()
预先递归它们(并相应地更改TreeFolder
),但是您必须递归—如果您愿意,您不能选择仅递归到左子树。 (在Haskell中,由于懒惰,我们不必进行权衡。)
这是一个粗略的草图,你可以通过一般和可扩展的方式来解决这个问题。 它不会直接在所有情况下都有效,但可能会帮助您入门。
首先,一些开始的假设:
- 我们不希望任何特定于
depth
东西添加到Tree
类中。 - 我们不想失去静态类型的好处。
关键是要意识到你想在这里重新创建的Haskell代码不是Tree
类型本身,而是它上面的模式匹配 。 因此,我们首先将“树上的模式匹配”本身作为第一类(ha,ha)实体。 使用C#-ish伪代码,因为我多年没有使用过Java:
interface MatchTree { R matchEmpty(Empty empty); R matchLeaf(Leaf leaf); R matchNode(Node node); }
要使用此reified模式匹配,我们需要在Tree
上使用适当的方法:
interface Tree { R patternMatch(MatchTree patterns); }
然后,每个单独的Tree
子类型都可以通过调用适当的MatchTree
方法并将其自身作为参数来实现该函数。
等效的Haskell将是这样的:
data MatchTree r = MatchTree { matchEmpty :: r , matchLeaf :: Int -> r , matchNode :: Tree -> Tree -> r }
…可以很容易地看到与案例表达直接对应:
match tree z fl fn = case tree of Empty -> z Leaf x -> fl i Node lt rt -> fn lt rt
这种风格的模式匹配在OOP圈子中被称为“访客模式”,顺便说一句。
例如,方法depth()可能与Tree没有直接关系,将它添加到class会破坏业务逻辑。 或者,更糟糕的是,Tree可能写在我无法访问的第三方库中。 在这种情况下,实现类似ADT的多态性的最简单方法是什么?
在这种情况下 – 我建议你使用设计模式访问者 。 它允许您分离数据表示和处理逻辑 ,甚至更多 – 它允许您实现不同的处理策略。
@stemm是正确的,访问者模式非常适合这个问题。 不过,我还建议您查看众所周知的访客模式的修改版本。 一位博主发明了这种教会编码模式 。 这种模式更加密集,并且具有比访问者模式更多的function风格。
编辑:这是你不想要的答案depth()
在Tree
界面中放置depth()
),但我认为它无论如何都值得进行全面分析。
更广泛地说,这是使用类实现sum类型的问题。 在面向对象的语言中有一种非常常见的方法来使用和类型。 即, 解释器模式 。
interface Tree { int depth(); } class Empty implements Tree { int depth(){ return 0; } class Leaf implements Tree { int n; int depth(){ return 1; } } class Node implements Tree { Tree l; Tree r; int depth(){ return max(depth(l), depth(r)); } }
让我们将它与haskell方法进行比较! 很明显,类的作者可以有任意多种类型( Empty
, Leaf
, Node
)和方法( depth()
, numLeafs()
)。 但是,想要扩展此树库的外部代码呢?
使用haskell中的代数数据类型,外部代码库可以添加类型:: Tree -> a
树函数,如果库公开Tree(..)
(类型本身和所有三个构造函数)。 但是,无法向Tree
添加新的构造函数,如下所示:
-- Code far far away can't do this in haskell data Tree = ... | ... | Node3 Tree Tree Tree
但是在java中使用解释器模式时,却恰恰相反。 无法向Tree
接口添加新方法,但可以添加一个新的构造函数,如下所示:
-- Code far far away *can* do this in java class Node3 implements Tree { Tree l; Tree mid; Tree r; int depth(){ ... } }
总之,如果符合以下条件,此设计模式非常有
- 其他人希望在代数数据结构中添加术语。
- 你希望全类型安全
但它有点令人不满意,因为:
- 其他人不能添加像
numNodes()
这样的reducer函数 - 人们觉得在每个class级都必须有一个关于树木的方法。 我们更喜欢每种方法对树进行一次匹配模式,而不是我们以一种转置方式进行。
我没有找到任何令人愉快的解决方案
我希望这可以帮到你。
interface Tree { } class Empty implements Tree { } class Leaf implements Tree { int n; } class Node implements Tree { Tree l; Tree r; } class Test{ public static void main (String args[]){ Test p = new Test(); Empty e = new Empty(); System.out.println(p.depth(e)); Leaf t = new Leaf(); System.out.println(p.depth(t)); Node n = new Node(); nl = t; nr = e; System.out.println(p.depth(n)); } int depth(Tree tree) { if(tree instanceof Leaf){ return 1; } return 0; } int depth(Node node) { return 1 + Math.max(depth(node.l), depth(node.r)); } } }
祝你好运!