Learning either tree and pattern matching through examples

Time:2019-11-28

Preface

In this installment, I’ll continue with either, using it to build a tree structure that allows me to simulate Scala’s pattern matching to build traversal methods.

Using generic data in Java, either becomes a single data structure that receives one of two types, which are stored in the left and right sections.

In the Roman digital parser example in the previous article, either saved an exception (on the left) or an integer (on the right), as shown in Figure 1:

Figure 1. Either abstracts one of two types

In this example, either is populated as follows:


Either<Exception, Integer> result = RomanNumeralParser.parseNumber("XLII");

Scala pattern matching

One of scala’s many great features is the ability to schedule using pattern matching (see resources). The demonstration is simpler than the description, so we’ll consider the function in Listing 1 to convert the number score to the letter score:

Listing 1. Scheduling letter scores based on scores using Scala pattern matching


val VALID_GRADES = Set("A", "B", "C", "D", "F")
def letterGrade(value: Any) : String = value match {
case x:Int if (90 to 100).contains(x) => "A"
case x:Int if (80 to 90).contains(x) => "B"
case x:Int if (70 to 80).contains(x) => "C"
case x:Int if (60 to 70).contains(x) => "D"
case x:Int if (0 to 60).contains(x) => "F"
case x:String if VALID_GRADES(x.toUpperCase) => x.toUpperCase
}
printf("Amy scores %d and receives %s\n", 91, letterGrade(91))
printf("Bob scores %d and receives %s\n", 72, letterGrade(72))
printf("Sam never showed for class, scored %d, and received %s\n", 
44, letterGrade(44))
printf("Roy transfered and already had %s, which translated as %s\n",
"B", letterGrade("B"))

In Listing 1, the entire body of the function consists of a match applied to value. For each option, pattern protection allows me to filter matches based on criteria other than parameter types. The advantage of this syntax is a clean option partition, rather than a series of complex if statements.

Pattern matching works with Scala’s case class, which is a class with special properties (including the ability to perform pattern matching) to eliminate decision sequences. Consider matching color combinations, as shown in Listing 2:

Listing 2. Matching case classes in Scala


class Color(val red:Int, val green:Int, val blue:Int)
case class Red(r:Int) extends Color(r, 0, 0)
case class Green(g:Int) extends Color(0, g, 0)
case class Blue(b:Int) extends Color(0, 0, b)
def printColor(c:Color) = c match {
case Red(v) => println("Red: " + v)
case Green(v) => println("Green: " + v)
case Blue(v) => println("Blue: " + v)
case col:Color => {
print("R: " + col.red + ", ")
print("G: " + col.green + ", ")
println("B: " + col.blue)
}
case null => println("invalid color")
}

In Listing 2, I create a basic color class, and then, like the case class, I create a special single color version. When deciding which color to pass to the function, I used match to match patterns based on all available options, including the last case, which handles null.

Java does not provide pattern matching, so it cannot replicate Scala’s ability to create clear and readable scheduling code. However, by using a combination of generic data structures and well-known data structures, we can achieve a closer capability, which brings me back to either.

Either tree

You can model a tree data structure with three abstractions, as shown in Table 1:

Empty Cell does not contain any values
Leaf Cell has a special data type value
Node Point to another leaf or node

But for convenience, I’ll use a class from the functional Java framework in this case. Conceptually, the either abstraction extends to the required aspects. For example, you can consider declaring either < empty, either < leaf, node > > which creates a three-part data structure, as shown in Figure 2:

Figure 2. Data structure of either < empty, either < lead, node > >

After executing three tree abstract implementation of either, I define the tree, as shown in Listing 3:

Listing 3. Either based tree


import fj.data.Either;
import static fj.data.Either.left;
import static fj.data.Either.right;
public abstract class Tree {
private Tree() {}
public abstract Either<Empty, Either<Leaf, Node>> toEither();
public static final class Empty extends Tree {
public Either<Empty, Either<Leaf, Node>> toEither() {
return left(this);
}
public Empty() {}
}
public static final class Leaf extends Tree {
public final int n;
public Either<Empty, Either<Leaf, Node>> toEither() {
return right(Either.<Leaf, Node>left(this));
}
public Leaf(int n) { this.n = n; }
}
public static final class Node extends Tree {
public final Tree left;
public final Tree right;
public Either<Empty, Either<Leaf, Node>> toEither() {
return right(Either.<Leaf, Node>right(this));
}
public Node(Tree left, Tree right) {
this.left = left;
this.right = right;
}
}
}

The abstract tree class in Listing 3 defines three final concrete classes: empty, leaf, and node. Internally, tree class uses three slots of either, as shown in Figure 2, to implement a rule that the leftmost slot always saves empty, the middle slot saves leaf, and the rightmost slot saves node. The method is: request each class to implement the toeither () method, and return the corresponding “slot” of this type. From the aspect of traditional computer science, every “unit” in data structure is a union, aiming to save one of three possible types at any given time.

Considering that the tree structure and its internal structure are based on the fact that < either, < left, node > > I can access every element of the tree by simulating pattern matching.

Pattern matching of tree traversal

Scala’s pattern matching encourages you to think about discrete situations. The left() and right() methods in the other implementation of functional Java implement the Iterable interface; this allows me to write code that supports pattern matching to determine the depth of the tree, as shown in Listing 4:

Listing 4. Checking the depth of the tree using a pattern matching like syntax


static public int depth(Tree t) {
for (Empty e : t.toEither().left())
return 0;
for (Either<Leaf, Node> ln: t.toEither().right()) {
for (Leaf leaf : ln.left())
return 1;
for (Node node : ln.right())
return 1 + max(depth(node.left), depth(node.right));
}
throw new RuntimeException("Inexhaustible pattern match on tree");
}

The depth () method in Listing 4 is a recursive depth lookup function. Because my tree is based on a specific data structure (< either, < left, node > >), I can treat each “slot” as a specific case. If the cell is empty, the branch has no depth. If the cell is a leaf, treat it as a tree level. If the cell is a node, I know that I should recursively search the left and right sides, then add 1 for another recursion.

I can also use the same pattern matching syntax to perform a recursive search of the tree, as shown in Listing 5:

Listing 5. Determining whether an element exists in the tree


static public boolean inTree(Tree t, int value) {
for (Empty e : t.toEither().left())
return false;
for (Either<Leaf, Node> ln: t.toEither().right()) {
for (Leaf leaf : ln.left())
return value == leaf.n;
for (Node node : ln.right())
return inTree(node.left, value) | inTree(node.right, value);
}
return false;
}

As before, I specify a return value for each possible “slot” in the data structure. If an empty cell is encountered, false is returned; my search fails. For leaves, I check the values passed and return true if they match. Otherwise, when I encounter a node, I will traverse the tree and use the | (non short circuited or operator) to combine the returned Boolean values.

To see the actual tree creation and search, consider the unit tests in Listing 6:

Listing 6. Test tree searchability


@Test
public void more_elaborate_searchp_test() {
Tree t = new Node(new Node(new Node(new Node(
new Node(new Leaf(4),new Empty()), 
new Leaf(12)), new Leaf(55)), 
new Empty()), new Leaf(4));
assertTrue(inTree(t, 55));
assertTrue(inTree(t, 4));
assertTrue(inTree(t, 12));
assertFalse(inTree(t, 42));
}

In Listing 6, I constructed the tree and then investigated whether there was an element. The instree () method returns true if one of the leaves is equal to the search value, and true propagates the recursive call stack because of the | (“or”) operator, as shown in Listing 5.

The example in Listing 5 determines whether the element appears in the tree. More complex versions also check the number of occurrences, as shown in Listing 7:

Listing 7. Find the number of times in the tree


static public int occurrencesIn(Tree t, int value) {
for (Empty e: t.toEither().left())
return 0;
for (Either<Leaf, Node> ln: t.toEither().right()) {
for (Leaf leaf : ln.left())
if (value == leaf.n) return 1;
for (Node node : ln.right())
return occurrencesIn(node.left, value) + occurrencesIn(node.right, value);
}
return 0;
}

In Listing 7, I return 1 for each matched leaf, which allows me to count the number of occurrences of each number in the tree.

Listing 8 shows the tests for depth (), instree (), and occurrencesin () in a complex tree:

Listing 8. Testing depth, presence, and number of occurrences in a complex tree


@Test
public void multi_branch_tree_test() {
Tree t = new Node(new Node(new Node(new Leaf(4),
new Node(new Leaf(1), new Node(
new Node(new Node(new Node(
new Node(new Node(new Leaf(10), new Leaf(0)),
new Leaf(22)), new Node(new Node(
new Node(new Leaf(4), new Empty()),
new Leaf(101)), new Leaf(555))),
new Leaf(201)), new Leaf(1000)),
new Leaf(4)))),
new Leaf(12)), new Leaf(27));
assertEquals(12, depth(t));
assertTrue(inTree(t, 555));
assertEquals(3, occurrencesIn(t, 4));
}

Because I apply regularity to the internal structure of the tree, I can analyze the tree during traversal by thinking about each situation, as shown by the element type. Although this syntax is not as powerful as the fully mature Scala pattern matching, it is surprisingly close to scala.

Concluding remarks

In this article, I introduced how to apply regularity to scala style pattern matching during tree traversal, and how to use some inherent attributes of generic Iterable, the ether class of functional Java and other elements to simulate powerful Scala functions.

The above is the whole content of this article. I hope it will help you in your study, and I hope you can support developepaer more.