Contract, Assertions, Phantom Types

Justin Dekeyser
Javarevisited
Published in
7 min readDec 1, 2020

In this text, we are going to investigate a simple design-by-contract case, with a compile-time crosscheck using the phantom type design pattern.

How to use phantom types to certify a contract.

The use case: mono or multi value

The use case we are going to handle is the representation of the data type

MonoOrMulti<T> =:= T | List<T>

When you enter that kind of world in Java, you quickly face the problem of union types. In Java, union of types is indeed something that is not possible to achieve. Future language upgrades will introduce the idea of sealed class, yet this idea is more like a union-by-restriction idea. In this text, we are going to investigate a much more flexible approach.

Problem resolution

Representing the union in the Java Type System

We first create a type to handle our union. We design this class to be very weak first, as we want it as generic as possible.

We expect the following contract to hold if ever it’d make sense:

assert (!isMonoValued && value instance List<@NonNull T> &&
value.stream.noneMatch(Objects::isNull))
|| (isMonoValued && value instanceof T)

So either the value is a non null T typed object (mono valued case), either the value is a non null List of T , which we require to be non null as well, with non-null elements.

We need here to be very clear: we do not expect a IO operation to directly create MonoOrMulti valued: developers will hold that responsibility. In other words, developers should guarantee the constructor is called within its contractual bounds.

Bounding creation via builders

A first thing we can do to enforce the correct creation of our MonoOrMulti valued, without altering its constructor, is to force the user of this class to create it using an indirection, as a factory or a builder.

Between factories or builders, we have made the choice of builders, for the flexibility it allows; We can indeed start with a top level builder

which gives access to a specific builder for a single value:

and another for specific multi value:

Although we split builders at the level of types, we can provide a unique implementation to centralize concerns, for a first sketch:

This polymorphic builder enforces two of the following paths, here written on String type class:

newBuilder(String.class)
.monoValued()
.buildFromValue("Hello world");
newBuilder(String.class)
.multiValued()
.addAll(List.of("Hello", "world"))
.build();

The above approach makes it possible to bound the various creative possibilities. The asserts statements in the builder code make it clear that our union type contract is fulfilled: we have a mono value element of type T , or an ordered collection of such.

Assert enforcement

So far, our builder is not null hostile, which is something we would like to have. In Java, null are part of the type system and one cannot avoid it using types, nor tweak a construction path as we did previously.

However, there is an idiomatic Java way to make it clear that we are null-hostile. The assert keyword may be used to enforce this behavior at a contractual level. The modifications are straightforward:

Java assertions are a perfect use case for contractual statements: an AssertionError (a sever error, in the Java failure system) will be thrown if the contract gets violated. As design-by-contract is a developer business, out of IO considerations, developers should focus enough to avoid them.

It also makes it very clear that we are coding at the contract level: throwing an (unchecked) exception will somehow hide this very particular and accurate semantic.

The problem with assert, and it’s a serious problem, is that it is turned off on production. This means that we should not rely on them. Well, this wouldn’t be a very serious business actually, as we (should) trust our developer's team!

There is a biggest problem assert are not handling: they bring contract information that are completely out of the type system. This is very clear in our example: once create, a user has a MonoOrMulti<T> instance, without any further information on whether it represents a mono, or a multi valued. This is exactly the problem we are going to solve with phantom types.

Phantom types as compile-time asserts

We are not going to remove any of our asserts. Rather, we are going to add them in the type system. How?

We are going to introduce a phantom type. As an example explains better than words, let’s directly go with code:

and for or MonoOrMulti structure class:

We introduced a dummy type parameter that is bounded to ValueKind ; The types that interest us are NullHostileMono and NullHostileOrderedMulti , as those are the concern of our problem.

On purpose, we have sketched a very complex type system at the level of ValueKind , to stress the idea that our pattern is in fact very flexible and efficient. This is due to the difference between interface and class inheritance in Java. If we wanted to encoded the dichotomy at a class level, we would require to subclass MonoOrMulti<T> , which is costly in terms of contracts: we need to keep contracts between child and parent classes coherent at every development step.

Second, the class inheritance hierarchy in Java is constrained by the single-parent principle: a class cannot have two parent classes.

This is very useful to keep internal state coherent, but in the problem we are concerned with, it is very a problem, as we want to profit from a diamond-graph type system.

Our builder needs to be refactored as well, to take the new constraints into account. We only repeat the interfaces here: the implementation update is trivial. For the mono builder, we update the buildFromValue method as

MonoOrMulti<T, ValueKind.NullHostileMono> buildFromValue(T value);

and for the multi builder, we only need to redefine the build method as

MonoOrMulti<T, ValueKind.NullHostileOrderedMulti> build();

When a user will now follow the road

newBuilder(String.class)
.monoValued()
.buildFromValue("Hello world");

he will have a compile-time level information that the obtained instance is of type NullHostileMono . Similarly, following the road

newBuilder(String.class)
.multiValued()
.addAll(List.of("Hello", "world"))
.build();

yield a NullHostileOrderedMulti element, and again: this information belongs the type system in a very flexible way.

The phantom type information is not here to replace the contracts guaranteed by assertions, but to reflect it at compile time, something assertions cann’t do in Java.

Binding contracts and phantom types

Somehow, our current setup is a bit flawed. Indeed, we do not have a so strong coupling between ValueKind markers and contracts. The assertions in the builder requires us to be kept up-to-date and they do not form a reusable set of feature.

We can go one step further, by binding contracts and phantom types. We first start to define a default method on the ValueKind type magnet, whose aim is to assert whether the default contract is valid or not.

Next, every child of ValueKind will override this default implement and reuse it to define its own contract:

Those contracts may now be used in the builder itself. Instead of reinventing the whole inside each method, we reuse the shared contract provided by the phantom types themselves. We only give one example to illustrate the idea:

Union evaluated method

As a final section, we provide a concrete example of what’s possible to achieve. Let’s say we want to provide an implementation of the method

message -> message if message does not contain " ",
[word for words in message] otherwise

Our knowledge of algebraic data types tells us that this is an exponential map String -> String | List<String> , hence can be represented through two maps String -> String and String -> List<String> , together with a way to distinguish the cases.

With our setup, it can be achieved as

As you see, the information that the MonoOrMulti instance is ordered and cannot contain nulls, are still present in the type system, even though we have lost part of it (mono or multi valued).

We still can recover the whole information, because we still have contracts. This requires, however, a human intervention:

From this assert, we contractually know that the value of result is a List<String> , hence we can continue with a cast operation and the computation pipe goes on!

That’s all for this text, sufficiently long already. I hope you enjoyed it and above all, I hope it will give you ideas or, on the contrary, make your current habits more robust to criticism!

--

--

Justin Dekeyser
Javarevisited

PhD. in Mathematics, W3C huge fan and Java profound lover ❤