Balzanka / guava-libraries

Automatically exported from code.google.com/p/guava-libraries
Apache License 2.0
0 stars 0 forks source link

Add type-safe maps #1346

Closed GoogleCodeExporter closed 9 years ago

GoogleCodeExporter commented 9 years ago
Please add this to Guava: http://blog.pdark.de/2010/05/28/type-safe-object-map/

In a nutshell, it's a map where can story any object as value under a key and 
get back the value without casting in a type-safe manner checked at compile 
time.

There are two possible implementations. One isn't 100% safe but extends the Map 
interface. 

The other one doesn't support get(Object) and remove(Object) from Map, so the 
resulting type just has a similar API but not one that is 100% Map compatible.

Original issue reported on code.google.com by adigu...@gmail.com on 20 Mar 2013 at 5:45

GoogleCodeExporter commented 9 years ago
What are some specific use cases for this?  We already provide 
ClassToInstanceMap and friends, which cover some of these areas.

Original comment by wasserman.louis on 20 Mar 2013 at 5:48

GoogleCodeExporter commented 9 years ago
The blogged API is not type safe.

map.get(new TypedMapKey<String>("key")) internally does an unchecked cast, but 
the cast isn't safe.

It is no different than:

@SuppressWarnings("unchecked")
<V> V get(Object key) {
  return (V) rawGet(key);
}

because in both ways the type of the value isn't guaranteed to be the type that 
caller thinks it is.

Original comment by be...@google.com on 24 Mar 2013 at 12:04

GoogleCodeExporter commented 9 years ago
In general, this seems to ask for a heterogeneous map in which having same-type 
type tokens doesn't imply that the type tokens are equal.

   i.e. For TypeToInstanceMap, there can be no more than one value to a type.

Essentially, give some composite key from:

   public static <K, B> Key<K, B> of(K key, Class<? extends B> clas) {...}

Above, key("1", String.class) would not be equal to key("2", String.class); all 
the while, the composite key would be a type token to whatever map uses it.

I'd assume this would be analogous to such a map: Map<K, TypeToInstanceMap<B>>; 
note that this could have multiple TypeToken<? extends B> to K, whereas care 
would be needed if you only wanted one token to a key.

For heterogeneous collections, say, lists, in general:

   ArrayList<TypeToInstanceMap<Object>> formalsToActuals; // maps are singletons
   ...
   list.get(0).put(String.class, "str");
   list.get(1).get(Double.class)

These approaches are work-arounds, mind you. But useable. Or, did I miss the 
point completely?

Original comment by jysjys1...@gmail.com on 24 Mar 2013 at 11:14

GoogleCodeExporter commented 9 years ago
I actually sort of like this, but in a variant form: the key should have no 
additional String parameter, and should use reference equality:

static final Key<String> ADDRESS_KEY = new Key<String>();

FWIW, the Java compiler uses essentially this idiom in the Context class 
(http://www.docjar.com/docs/api/com/sun/tools/javac/util/Context.html), though 
it binds factories instead of objects themselves in what I'd describe as a poor 
man's dependency injection.

Original comment by lowas...@google.com on 3 May 2013 at 9:11

GoogleCodeExporter commented 9 years ago
> The blogged API is not type safe.

This is intentional. I needed my map to be a drop in replacement for 
java.util.Map.

For Guava, I suggest to implement it differently and maybe support a wrapper 
helper that converts between TypedMap and Map.

> the key should have no additional String parameter

I considered this but it makes debugging a nightmare since you can't tell 
anymore what each key was (there is no way to return to the Key instance from 
within the map).

If Java only had a hook for the part of the compiler that handles "enum" :-(

I also considered using enums for this but the API became too clumsy.

One last comment: Someone suggested to swap the API:

Key<String> ADDRESS_KEY = new Key<String>();
String value = ADDRESS_KEY.getFrom( map );

I don't like it much. It deviates quite a bit from the Map API that we're used 
to. But it might be a case of "reject great new idea because of tradition".

Original comment by adigu...@gmail.com on 5 May 2013 at 2:42

GoogleCodeExporter commented 9 years ago
Did not know that this had replies...

I'll add that I've worked with, what I'd call, a more general solution that, 
while a bit involved, does well enough with this problem.

Consider a structure:

    class Actual<T> {
        final T value;
        final TypeToken<T> formalType;

        TypeToken<T> getActualType() {
            // memoize, since Actual is likely to be an immutable class
            return value==null? formalType: formalType.getSubtype(value.getClass());
        }
    }

The above effectively types a value and may even preserve the generic type of 
an actual type given a generic formal type:

    List<String> list = new LinkedList<String>();
    Actual.of(list, Object.class).getActualType(); // LinkedList
    Actual.of(list, new TypeToken<List<String>>(){}).getActualType(); // LinkedList<String>

Use actuals as values to maps (Map<K, Actual<? extends V>>), and, if there is 
ever an interface or subclass of Actual, perhaps go the route of the Multimaps 
and provide a Supplier:

    interface ActualMap<K, V> extends Map<K, V> {
       ...
       Map<K, Actual<? extends V>> asMapOfActuals();
       // Likely fully-operational if Actual is a final class
    }

An implementation to ActualMap<K, V> may find it advantageous to explicitly 
aggregate the V parameter type as a TypeToken; put(K, V) for maps with a 
generic value type parameter could possibly preserve generic-type values; 
otherwise, maps whose values are known to be generic would not be able to store 
put(K, V) values as generic values:

    V put(K key, T value, TypeToken<T> typeToken) ...

    V put(K key, V value) {
        return put(key, value, preservedType.getSubType(value.getClass()));
        // may deal with warnings
    }

Lastly, concerning the ActualMap API, I had personally opted for 
Predicate/Function usage for contains/(get|remove) respectively:

contains(Object, TypeToken<?>, BiPredicate<? super TypeToken<?>, ? super 
Actual<? extends V>>)
contains(Object, Predicate<? super Actual<?>>)
[get|remove](Object, TypeToken<T>, BiFunction<? super TypeToken<T>, ? super 
Actual<? extends V>, ? extends T>)
[get|remove](Object, Function<? super Actual<? extends V>, ? extends T>)

I defined common predicates and functions in a Actuals utility class:

    equalsFormalType()           : BiPredicate<TypeToken<?>, Actual<?>>
    isAssignableFromFormalType() : BiPredicate<TypeToken<?>, Actual<?>>
    equalsActualType()           : BiPredicate<TypeToken<?>, Actual<?>>
    isAssignableFromActualType() : BiPredicate<TypeToken<?>, Actual<?>>

    equalsFormalType(TypeToken<?>)           : Predicate<Actual<?>>
    isAssignableFromFormalType(TypeToken<?>) : Predicate<Actual<?>>
    equalsActualType(TypeToken<?>)           : Predicate<Actual<?>>
    isAssignableFromActualType(TypeToken<?>) : Predicate<Actual<?>>

    castActualValue()             : BiFunction<TypeToken<T>, Actual<? extends U>, T>
    castActualValue(TypeToken<T>) : Function<Actual<? extends U>, T>

Finally, to support usage, I defined a key/type-token entry type, Option<K, V> 
when defining static final keys:

    public static final Option<String, Integer> ID = Option.of("id", Integer.class); // 
    public static final String MEASURES = "measures"; // could be one of [Iterable<Double>, double[]]

An operation in a utility class could be used to adapt maps to actual maps:

    static ActualMap<K,V> of(Map<K, V> map) {
        if (map instanceof ActualMap<?, ?>)
            return (Actual<K, V>) map;
        final Actual<K, V> m = new LossyActualMap<>();
        m.putAll(map);
        return m;
    }

This was a bit involved in implementing, frankly, but I feel as if it works as 
intended, and gives a be of flexibility as to how the types of values are 
viewed before retrieving or removing them (TypeToInstanceMaps can't given that).

Original comment by jysjys1...@gmail.com on 22 Aug 2013 at 12:26

GoogleCodeExporter commented 9 years ago
Of course, the aforementioned Context works as well if willing to sacrifice 
flexibility in key parameter type.

Original comment by jysjys1...@gmail.com on 22 Aug 2013 at 12:31

GoogleCodeExporter commented 9 years ago
Actual/ActualMap looks interesting but IMO solves a different problem: Type 
preservation. My approach allows you to put Actuals into a typed map, so it's 
more flexible since it doesn't impose an artificial limit for the value type.

Places where I used this:

- Options / preferences to distinguish between int, boolean and String values.

- data elements in models which allow to attach arbitrary data to an instance 
(SWT, Swing and ZK use this but without type safety).

The biggest advantage of the constant keys is that it's easy to find all places 
where a value is being used (as opposed to String constants).

Original comment by adigu...@gmail.com on 7 Sep 2013 at 6:27

GoogleCodeExporter commented 9 years ago
This issue has been migrated to GitHub.

It can be found at https://github.com/google/guava/issues/<issue id>

Original comment by cgdecker@google.com on 1 Nov 2014 at 4:12

GoogleCodeExporter commented 9 years ago

Original comment by cgdecker@google.com on 1 Nov 2014 at 4:18

GoogleCodeExporter commented 9 years ago

Original comment by cgdecker@google.com on 3 Nov 2014 at 9:08