Key-Value Type Correlation

We’ve already discussed a maps whose value types depended on key values. Today’s topic is slightly different, and is about value types depending and key types.

Now that types may be parametrised with generics, it is a common requirements that types may be correlated, that is, share an unspecified parameter. For instance you could map pairs of classes to conversion functions:
For an input pair

Pair<Class<Fruit>,Class<Juice>> mixerSpecification = Pair.of(Fruit.class, Juice.class);

we want a converter

Function<Fruit,Juice> mixer;

that we can use as follows:

Fruit orange = ...;
Juice orangeJuice = mixer.apply(orange);

In other words the input type Pair<Class<F>,Class<T>> is correlated with the output type Function<F,T> as the input type parameters F and T are arbitrary, but must be repeated in the output type parameters.

In addition to adding and getting converters we’ll also see how we can merge converter libraries, that is how a putAll method can be implemented in the various implementations.

Level Seven (or Three)

The most accurate Java type we can use for this requirement is:

Map<Pair<Class<?>, Class<?>>, Function<?, ?>> converters = new HashMap<>();

Adding a new converter is done as follows:

Function<Fruit,Juice> mixer = ...;
converters.put(Pair.<Class<?>,Class<?>>of(Fruit.class, Juice.class),
    mixer);

Note that we had to force the Pair constructor function to build a Pair<Class<?>,Class<?>>, because the inferred type would otherwise be Pair<Class<Fruit>,Class<Juice>>, which can’t be cast (as required by the required by the Map.put method) to the Map‘s key type parameter that we set to Pair<Class<?>,Class<?>>.

There is no compile-time or run-time verification of the correlation between the key and the value types, simply because we could not specify it in the Map type specification.

Retrieving the converter given a pair of classes is done as follows:

@SuppressWarnings("unchecked")
Function<Fruit,Juice> mixer = (Function<Fruit,Juice>)
    converters.get(Pair.of(Fruit.class,Juice.class));

Note that we do not need to construct a Pair<Class<?>,Class<?>> this time, as Map.get takes an Object parameter. We however must cast the result from Function<?,?> (the Map‘s declared value type), which generates an “unchecked conversion” warning that we have to suppress with an annotation.

Although errors in types will be caught neither at compilation nor at run-time, this design could arguably be qualified to be Level Three. See the following code containing mistakes:

Function<Fruit,Colour> getColour = ...;
converters.put(Pair.<Class<?>,Class<?>>of(Fruit.class, Juice.class),
    getColour);
//...
@SuppressWarnings("unchecked")
Function<Fruit,Colour> getColour = (Function<Fruit,Colour>)
    converters.get(Pair.of(Fruit.class,Juice.class));

Are the two mistakes (mismatch between Colour and Juice) “visually obvious”? In the first case there may be other things creeping up between the declaration of getColour (which could e.g. be a method parameter) and its use in the put method. The second one suppresses warnings, which generally means any Level Two design in use in the scope of the annotation is downgraded to Level Seven. As there is no code beside the Pair construction and get invocation, it should however be safe as long as no enterprising programmer moves all such annotations up to method or class level to save lines of code.

The putAll method specified in the requirements of this post is very easy to write:

Map<Pair<Class<?>,Class<?>>, Function<?,?>> externalLibrary=...;
converters.putAll(externalLibrary);

However it provides no guarantees as to what internal constraints exist in externalLibrary. If they do not match those of converters, the internal consistency of converters will be destroyed, potentially resulting in hard-to-track errors.

Contain Unsafe Code; Keep Outside Code Level One

One way of addressing issues stemming from “unsafe” code is to gather it in a single class with an interface enforcing desired type constraints, to avoid bugs appearing in a copy-paste-modify sequence. In this case it means encapsulating the Map in a class like this:

public class ConverterLibrary {
    private Map<Pair<Class<?>, Class<?>>, Function<?, ?>> converters = new HashMap<>();

    @SuppressWarnings("unchecked")
    public <F,T> Function<F,T> put(Class<F> from, Class<T> to, Function<? super F, ? extends T> converter) {
        return (Function<F,T>) converters.put(Pair.<Class<?>, Class<?>>of(from, to), converter);
    }

    @SuppressWarnings("unchecked")
    public <F,T> Function<F,T> get(Class<F> from, Class<T> to) {
        return (Function<F,T>) converters.get(Pair.of(from, to));
    }
    
    public boolean putAll(ConverterLibrary that) {
        return this.converters.putAll(that.converters);
    }
}

As a bonus we made the put method more tolerant on values. This permits passing more general functions:

Function<Object,String> toString = ...;
converterLibrary.put(Fruit.class, String.class, toString);

Note that it is critical the map be private, and never returned from any method (don’t put an asMap method returning a writable Map!! This is to ensure subclasses and clients may not corrupt its internal consistency. Now, the content of the ConverterLibrary class is still Level Three (any change must be carefully examined for “visually obvious” mistakes), but its clients are Level One:

Function<Fruit,Colour> getColour = ...;
converters.put(Fruit.class, Juice.class, getColour);
//...
Function<Fruit,Colour> getColour = converters.get(Fruit.class, Juice.class);

Both line two and line four will trigger compilation errors.

One thought on “Key-Value Type Correlation”

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong> <pre lang="" line="" escaped="" cssfile="">