When you have a predicate that acts on a tree data structure, rather than return a Boolean, you should return an object that represents success/failure and carries explanatory information in the failure case.
That is, rather than:
type tree val pred : tree -> bool
You should write:
type tree type result = | Pass | Fail of (* error message or data *) val pred : tree -> result
With the failure case explaining what failed, where, and why.
Why? Because otherwise you will find yourself staring at a Sentry log
where some process failed, and all you have to go on is a solitary
value. And it can be hard to debug locally.
Example: if you have a function that compares two trees for equality on an exact, bit-by-bit basis, then if the trees are potentially deeply nested you might want the comparison function to tell you which subtrees don’t match and where in the tree the mismatch happens. In this particular case you don’t need to know why the predicate failed (since you’re doing strict equality), but you want to know where the failure happened and what are the values that don’t match.
Example: sometimes you want to implement something like relaxed
equality—for example, you might have a data type that represents mathematical
expressions, and you want to compare them for equality while applying certain
equivalence rules, like accepting
a + b and
b + a as equal, or
x + 0 as
x. In cases where the predicate is not trivial, you want to know not
just where and what subtrees don’t match but also why.