Key-Value Type Correlation, Generic Version

In my previous post I explored how we can have the type of a map value be correlated with the type of its key.

We reached a design where a class contains a Level Three implementation, with Level One client code.This is all well and good, but in a real project this design will probably expand in two directions simultaneously:

  • Instances: we may need to copy-paste-modify the ConverterLibrary (that maps Pair<Class<F>,Class<T>> to Function<F,T>) class to create libraries of formatters (Class<T> to Parse<T>), parsers, CRUD services, etc. The keys need not be class objects: we can have prototype maps (T to T), maps associating List<T> to keys of type Supplier<T> (e.g. recording values previously returned by the supplier or inversely that the given supplier needs to return), etc.

  • Features: Clients may need methods like containsKey, remove, keySet, etc. This entails dangerously writing more Level Three code, potentially copy-pasting from one instance to another (previous point), potentially forgetting to update all parts of the code.

Can we capture the pattern of ConverterLibrary in a single class, with a single implementation of put, get, putAll but also remove, containsKey, size etc, all that without specifying the Pair<Class...> and Function stuff, so that it may be safely instantiated for all the above examples?
The trick is to recognise that there are two ways in Java to enforce correlation between two type parameters.
The first way, that was used by the methods in the previous section, is to use type parameters in a method:

public <F,T> void put(
    Class<F> from,
    Class<T> to,
    Function<F,T> converter
);

In this signature, the two type parameters permit enforcing a correlation between the key and value type parameters.
The other way, that we are going to use now, is in the type parameters of a type (class or interface), which may enforce correlations between types of its attributes (yet another way, is correlations between parameter of superclass type parameters). This is a class written to look as much as possible like the above method:

public class Put<F,T> {
    Class<F> from;
    Class<T> to;
    Function<F,T> converter;
}

In addition to being a suitable parameter to a generic put method, an instance of this class can be used to represent an entry of a converter map, the map itself being a collection of such entries. Renaming the above and rewriting to use a generic Entry superclass we get this:

public class Entry<K,V> {
    public final K key;
    public final V value;
    public Entry(K key, V value) {
        this.key=key;
        this.value=value;
    }
}
public class ConverterEntry<F,T> extends Entry< Pair<Class<F>,Class<T>>, Function<F,T> > {}

As this class not only records the key and value types, but also all correlations between them, we know it is sufficient to use it as type parameter:

public class CorrelatedMap<E> {...}
public class ConverterLibrary extends CorrelatedMap<ConverterEntry<?,?>> {...}

If we don’t need a return value to the put method, it may be declared like this:

public class CorrelatedMap<E> {
    public void put(E entry) {...}
    //...
}

But how can we get a type-safe get method that would cause a compilation error if misused?
We could do this:

public E get(E entry) {
   // return an entry having the same key as the given one.
}

That works fine, but when passing a ConverterEntry<Fruit,Juice> we still get a ConverterEntry<?,?> in return because that’s what E is set to in ConverterLibrary.
OR, we could do the same but with the following signature:

public <K,V> Entry<K,V> get(Entry<K,V> entry);

But then when passing a ConverterEntry<Fruit,Juice> we get an Entry<Pair<Class<Fruit>,Class<Juice>,Function<Fruit,Juice>>. Somewhat better (as, assuming a getValue() method in Entry, we can get the Function without casting back and forth). However there’s no verification we’re actually passing a ConverterEntry.
Can we do intersection types?

public <K, V, T extends E & Entry<K,V>> T get(T entry);

No, the compiler complains that T attempts to derive both Entry<?,?> (because that’s what E resolves to) and Entry<K,V>.

All these problems stem from Entry appearing in the CorrelatedMap having type parameters, that we’re forced to set to “?“. The trick is therefore to create a superinterface without type parameters:

Entry types:

public interface HasKey {
    public Object getKey();
}
public class Entry<K,V> {
    public final K key;
    public final V value;
    public Entry(K key, V value) {
        this.key=key;
        this.value=value;
    }
    public K getKey() { return key; }
}
public interface ConverterEntry extends HasKey {}
public class TypedConverterEntry<F,T> extends Entry< Pair<Class<F>,Class<T>>, Function<F,T> > {
    public TypedConverterEntry(Class<F> from, Class<T> to) {
        super(Pair.of(from, to), null);
    }
    public TypedConverterEntry(Class<F> from, Class<T> to, Function<F,T> converter) {
        super(Pair.of(from, to), converter);
    }
}

Map types:

public class CorrelatedMap<E extends HasKey> {
    Map<Object, E> entries = new HashMap<>();
    public <T extends E> T get(T query) {
        return (T)entries.get(query.getKey());
    }
    public <T extends E> T put(T entry) {
        return (T)entries.put(entry.getKey(), entry);
    }
    public boolean putAll(CorrelatedMap<E> that) {
        return entries.putAll(that.entries);
    }
    //...
}
public class ConverterLibrary extends CorrelatedMap<ConverterEntry> {
    public <F,T> Function<F,T> get(Class<F> from, Class<T> to) {
        return getValue(super.get(new TypedConverterEntry<>(from,to)));
    }
    public <F,T> Function<F,T> put(Class<F> from, Class<T> to, Function<F,T> converter) {
        return getValue(super.put(new TypedConverterEntry<>(from, to, converter)));
    }
    private static <F,T> Function<F,T> getValue(TypedConverterEntry<F,T> e) {
        return (e==null)? null : e.value;
    }
}

Methods in ConverterLibrary are there for convenience, to avoid its clients having to construct and deconstruct TypedConverterEntries, but unlike the previous implementation it is Level One: the (Class<F>, Class<T>) parameter types force the TypedConverterEntry to type TypedConverterEntry<F, T>, which is assigned to the type parameter E of the put method, which returns the same TypedConverterEntry<F, T> type, whose value attribute is correctly inferred to have type Function%lt;F, T>.

I am not very happy with having to define two types ConverterEntry and TypedConverterEntry, as well as having to invoke getValue from the subclass ConverterLibrary instead of the superclass. Is it possible to do better while still retaining Level One safety? I do not yet have the answer to that question.

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="">