COMP 310
|
Lec21: Generics and Extended Visitors Together |
![]() ![]() ![]() ![]() |
Today, we are going to try to wrap up some loose ends and try tie extended visitors and generics together.
Below is the code for the deletion algorithm. Things to note:
/** * DeleteNAlgo - deletes the supplied parameter from a TreeN * preserving balance with a maximum number of elements per node. */ public class DeleteNAlgo implements ITreeNAlgo { /** * Splits child upwards if its state > order and splices it into * its parent. The supplied param is an ISpliceCmd used to do the * splicing. */ private SplitUpAndApply splitUpAndSplice; /** * Constructor for the class * @param order -- the max possible number of elements in a node */ public DeleteNAlgo(int order) { splitUpAndSplice = new SplitUpAndApply(order); } public Object caseAt(int s, final TreeN host, Object... keys) { final Object key = keys[0]; switch(s) { case 0: { return key; // no-op for empty case } case 1: { collapse2Node(host); }// fall through to default case default: { return host.execute(new ITreeNAlgo() { public Object caseAt(int s_help, final TreeN h, Object... cmds) { switch(s_help) { case 0: { return key; } case 1: { if (h.getDat(0).equals(key)) { //System.out.println("Found data element!"); Object d = h.getDat(0); // get key h.splitDownAt(0); //transition to empty return d; } else { //System.err.println("Element "+ key +" not in tree!"); ((ILambda)cmds[0]).apply(h); return h.getDat(0); } } default : { final int x = findX(h, s_help, ((Integer)key).intValue()); TreeN newChild = collapse2Node(h.splitDownAt(x).getChild(x)); // push data down Object result = newChild.execute(this, new ILambda() { /** * @param child a TreeN subtree of h. */ public Object apply(Object... children) { return h.spliceAt(x, (TreeN)children[0]); } }); h.execute(splitUpAndSplice, cmds[0]); return result; } } } } , new ILambda() { // Top-level no-op lambda public Object apply(Object... nu) { return host; } }); } } } //------ Utility methods ------------------------------------------------------ /** * Utility method to collapses a 2-node tree by splicing it with * its two children. * @param t -- must be a 2-node tree! */ private final TreeN collapse2Node(TreeN t) { t.spliceAt(1,t.getChild(1)); t.spliceAt(0,t.getChild(0)); return t; } /** * Utility method that finds the index of the data element that * either matches the supplied key or is the first element bigger * than the key. * @param t - an TreeN * @param state - the state of the tree * @param k - the key to locate * @return - the index of the data element such that if k exists in the * tree, it is guaranteed to be in the 2-node tree * defined by the data at the index and its left and right children. */ private final int findX(TreeN t, int state, int k) { for(int i = 0;i< state; i++) if(t.getDat(i).intValue()>=k) return i; return state-1; } }
While the switch statement implementation of the extended visitors worked just fine in the above example, the problem with any sort of if-else stack-based implementation, such as a switch statement, is that while it is generally quite fast, it is not very flexible or extensible. It is quite a common design trade-off where speed trades for flexibility. In general, delegation-based architectures will give you better flexibility and extensibility, but for smaller, less complex systems, the overhead of the extra method calls may hurt performance. The break-even point in performance comes when the conditional logic takes as much computing time as a method dispatch. Beyond that point, delegation-based systems will win on both speed and flexibility counts.
So, instead, we consider this notion:
A visitor is a collection of semantically related lambdas.
That is, overall, a visitor has a certain semantic, a meaning or utility. Each case of the a visitor is the implementation of that semantic for its associated host. If we say that a method is nothing more than a lambda function held under the closure created by its class, then why can't we represent a visitor as a physical collection of lambda objects?
If we use a dictionary (java.util.Map) to hold our lambdas, we can associate an index (the key) with a lambda (the value). The parameterized caseAt method of an extended visitor thus becomes merely a process of taking the given index value, using it to retrieve the associated lambda from the dictionary and then executing that lambda. If no associated lambda can be found, then simply run a default lambda.
This lambda-based implementation gives us complete flexibility in dynamically manipulating the cases the visitor can handle. It also fundamentally changes the process of creating a visitor from one of defining cases in code at compile-time to one of loading lambdas at run-time.
Rolling generics into the a lambda-based extended visitor is a non-trivial task. The current state of the code is a "work in progress" still.
See the HW05 assignment page documentation and code for the latest generic lambda-based extended visitor implementation.
Here's how the above self-balancing tree deletion algorithm changes when implemented with a generic lambda-based extended visitor. Things to note:
The code uses an older implementation of the generic extended visitors, so the syntax used is slightly different than that used in HW05. Please focus less on the generics syntax and more on how the algorithm is constructed.
Fundamentally, the algorithm is identical to the above, switch statement implementation.
The entire code consists of the only the constructor code, where the lambdas are loaded.
Since the deletion algo was not meant to be modified at run-time, all the lambdas are loaded in the constructor, but the lambda-based extended visitor framework provides methods to manipulate the lambdas at any time.
The caseAt method is missing because it is invariant and is inherited from the abstract AExtVisitor superclass via the immediate ATreeNAlgo parent.
How an initializer block is used in place of a constructor in the anonymous inner class implementation of the helper algorithm.
The trick of using a field assigned to "this" in the anonymous inner class ATreeNAlgo to enable the further nested anonymous inner class commands to reference it. Without this trick, Java can only reference the most outer class (DeleteNAlgo2) and the most nested anonymous inner class (the helper's lambdas) but not any intermediate nested inner classes, e.g. the helper algo itself.
/** * DeleteNAlgo - deletes the supplied parameter from a TreeN * preserving balance with a maximum number of elements per node. * * Utilizes lambda-based extended visitors */ public class DeleteNAlgo2extends ATreeNAlgo { /** * Splits child upwards if its state > order and splices it into * its parent. The supplied param is an ISpliceCmd used to do the * splicing. */ private SplitUpAndApply splitUpAndSplice; private Comparator comp = new Comparator () { public int compare(E o1, E o2) { return ((Comparable )o1).compareTo(o2); } }; /** * Constructor for the class * @param order -- the max possible number of elements in a node */ public DeleteNAlgo2(int order) { System.out.println("DeleteNAlgo2 instantiated. order = "+order); splitUpAndSplice = new SplitUpAndApply (order); setCmd(0, new IExtVisitorCmd >(){ public E apply(Integer index, final TreeN host, final E... keys) { return keys[0]; // no-op for empty case } }); setCmd(1, new IExtVisitorCmd >(){ public E apply(Integer index, final TreeN host, final E... keys) { collapse2Node(host); return getDefaultCmd().apply(index, host, keys); // rest is same as default } }); setDefaultCmd(new IExtVisitorCmd >(){ public E apply(Integer index, final TreeN host, final E... keys) { return host.execute(new ATreeNAlgo ,TreeN >>() { private ATreeNAlgo ,TreeN >> this_helper = this; //Initializer block { setCmd(0, new IExtVisitorCmd ,TreeN >, TreeN >(){ public E apply(Integer index, final TreeN h, final ILambda ,TreeN >... cmds) { return keys[0]; } }); setCmd(1, new IExtVisitorCmd ,TreeN >, TreeN >(){ public E apply(Integer index, final TreeN h, final ILambda ,TreeN >... cmds) { if (h.getDat(0).equals(keys[0])) { //System.out.println("Found data element!"); E d = h.getDat(0); // get key h.splitDownAt(0); //transition to empty return d; } else { //System.err.println("Element "+ key[0] +" not in tree!"); cmds[0].apply(h); return h.getDat(0); } } }); setDefaultCmd(new IExtVisitorCmd ,TreeN >, TreeN >(){ public E apply(Integer s_help, final TreeN h, final ILambda ,TreeN >... cmds) { final int x = findX(h, s_help, keys[0]); TreeN newChild = collapse2Node(h.splitDownAt(x).getChild(x)); // push data down E result = newChild.execute(this_helper, new ILambda , TreeN >() { /** * @param child a TreeN subtree of h. */ public TreeN apply(TreeN ... child) { return h.spliceAt(x, child[0]); } }); h.execute(splitUpAndSplice, cmds); return result; } }); } }, new ILambda , TreeN >() { public TreeN apply(TreeN ... nu) { return host; } }); } }); } //------ Utility methods ------------------------------------------------------ /** * Utility method to collapses a 2-node tree by splicing it with * its two children. * @param t -- must be a 2-node tree! */ private final TreeN collapse2Node(TreeN t) { t.spliceAt(1,t.getChild(1)); t.spliceAt(0,t.getChild(0)); return t; } /** * Utility method that finds the index of the data element that * either matches the supplied key or is the first element bigger * than the key. * @param t - an TreeN * @param state - the state of the tree * @param k - the key to locate * @return - the index of the data element such that if k exists in the * tree, it is guaranteed to be in the 2-node tree * defined by the data at the index and its left and right children. */ private final int findX(TreeN t, int state, E k) { for(int i = 0;i< state; i++) { if(0 <= comp.compare(t.getDat(i),k)) return i; } return state-1; } }
© 2010 by Stephen Wong