A linked list is either
In Java we can represent this situation using a "sealed interface."
// A linked list is either empty or not empty.
sealed interface LinkedList<T>
permits Empty, NotEmpty {}
record Empty<T>() implements LinkedList<T> {}
record NotEmpty<T>(T first, LinkedList<T> rest) {}To define operations on a linked list we need to define their operation on the "empty" and "not empty" cases.
The size of an empty list is 0.
int size(LinkedList<?> ll) {
return switch (ll) {
case Empty() -> 0;
...
};
}And the size of a not empty list is 1 + the size of the rest of the list.
int size(LinkedList<?> ll) {
return switch (ll) {
case Empty() -> 0;
case NotEmpty(_, var rest) -> 1 + size(rest);
};
}To "map" a function on each element in the list works very similarly.
<T, R> LinkedList<R> map(
LinkedList<T> ll,
Function<? super T, ? extends R> f
) {
return switch (ll) {
case Empty() -> new Empty();
case NotEmpty(var first, var rest) -> new NotEmpty(
f.apply(first),
map(rest, f)
);
};
}As does every operation you might want to define. The recursive structure of the list itself is your guide for implementing operations on it.
A tree is either
In Java we can represent this situation using a "sealed interface."
// A tree is either a leaf or a branch.
sealed interface BST<T>
permits Leaf, Branch {}
record Leaf<T>() implements BST<T> {}
record Branch<T>(BST<T> left, T value, BST<T> right) {}To insert a value into a BST we need to be careful to always properly order elements. If a branch has 5 and we try to insert 2 in the tree, the 2 should go to the "left" because it is less than 5.
<T> BST<T> insert(
BST<T> bst,
T value,
Comparator<? super T> comparator
) {
return switch (bst) {
case Leaf() -> new Branch(
new Leaf(),
value,
new Leaf()
);
case Branch(var left, var v, var right) -> {
int comparison = comparator.compare(value, v);
if (comparison == 0) {
yield bst;
}
else if (comparison > 0) {
yield new Branch(
left,
v,
insert(right, value, comparator)
);
}
else {
yield new Branch(
insert(left, value, comparator),
v,
right
);
}
}
};
}This is the invariant that allows us to quickly search for values in such a structure. At every branch we can ignore at most half of the tree.
<T> boolean contains(
BST<T> bst,
T value,
Comparator<? super T> comparator
) {
return switch (bst) {
case Leaf() -> false;
case Branch(var left, var v, var right) -> {
int comparison = comparator.compare(value, v);
if (comparison == 0) {
yield true;
}
else if (comparison > 0) {
yield contains(
left,
value,
comparator
);
}
else {
yield contains(
right,
value,
comparator
);
}
}
};
}Now compare this explanation and whatever one you - the reader - were given in school. What are your thoughts?