In other situations, the approach of using an abstract datatype like this can actually rule out some invalid states, and I allude to that in the penultimate section of the blog post where I talk about using abstract types to “fake” parsers from validation functions. However, even that is still different from the technique focused on in most of the post. To illustrate why, consider an encoding of the NonEmpty type from the blog post in Java using an abstract type:
public class NonEmpty<T> {
public final ImmutableList<T> list;
private NonEmpty(ImmutableList<T> list) {
this.list = list;
}
public static <T> Optional<NonEmpty<T>> fromList(List<T> list) {
return list.isEmpty()
? Optional.none()
: Optional.of(new NonEmpty<>(ImmutableList.copyOf(list)));
}
public T head() {
return list.get(0);
}
}
In this example, since the constructor is private, a NonEmpty<T> can only be created via the static fromList method. This certainly reduces the surface area for failure, but it doesn’t technically make illegal states unrepresentable, since a mistake in the implementation of the NonEmpty class itself could theoretically lead to its list field containing an empty list.In contrast, the NonEmpty type described in the blog post is “correct by construction”—it genuinely makes the illegal state impossible. A translation of that type into Java syntax would look like this:
public class NonEmpty<T> {
public final T head;
public final ImmutableList<T> tail;
public NonEmpty(T head, ImmutableList<T> tail) {
this.head = head;
this.tail = tail;
}
public static <T> Optional<NonEmpty<T>> fromList(List<T> list) {
return list.isEmpty()
? Optional.none()
: Optional.of(new NonEmpty<>(list.get(0), ImmutableList.copyOf(list.subList(1, list.size()))));
}
}
This is a little less compelling than the Haskell version simply because of Java’s pervasive nullability and the fact that List is not an inductive type in Java so you don’t get the exhaustiveness checking, but the basic ideas are still the same. Because NonEmpty<T> is correct by construction, it doesn’t need to be an abstract type—its constructor is public—in order to enforce correctness.